How to wait for a Redux action to change state from a React component

Viewed 4215

I'm in a component which has a prop currentLineIndex passed by its parent container and coming from a Redux reducer.

In the same component's function I update currentLineIndex with an action creator and then I want to scroll to the new currentLineIndex. But it's not already updated, so I scroll to the same line.

I've tried using async / await as you'll see but it's not working.

In my component:

const { currentLineIndex, setCurrentLineIndex } = props; // passed by the parent container 

const handlePlaybackEnd = async () => {
  const nextIndex = currentLineIndex + 1;
  // await don't wait until global state / component props gets updated
  await setCurrentLineIndex(nextLineIndex);
  // so when I scroll on next line, I scroll to the same line.
  scrollToCurrentLine(); 
};

const scrollToCurrentLine = () => {
  const currentLineEl = document.getElementById(currentLineIndex);
  currentLineEl.scrollIntoView({ block: 'start', behaviour: 'smooth' });
};

in actions/index.js:

export function setCurrentLineIndex(index) {
  return { type: SET_CURRENT_LINE_INDEX, payload: index };
}

in my reducer:

case SET_CURRENT_LINE_INDEX:
  return {
    ...state,
    currentLineIndex: action.payload,
  };

Action and reducers are working good and my component state is successfully updated, but it's already too late.

I really need to rely on Redux state, not just to pass the currentLineIndex to scrollToCurrentLine(), that would be too easy :)

What would be the best solution to wait until my component state has been updated ?

3 Answers

I finally solved this by making my component a class component so I could use componentDidUpdate

componentDidUpdate(prevProps) {
  if (prevProps.currentLineIndex !== this.props.currentLineIndex) {
    this.scrollToCurrentLine(this.props.currentLineIndex);
  }
}

handlePlaybackEnd = () => this.props.setCurrentLineIndex(this.props.currentLineIndex + 1);

2020 UPDATE

Hooks made it a lot simpler, no need for a class component, just use an effect:

const scrollToCurrentLine = useCallback(() => {
  const currentLineEl = document.getElementById(currentLineIndex);
  currentLineEl.scrollIntoView({ block: 'start', behaviour: 'smooth' });
}, [currentLineIndex]);

useEffect(scrollToCurrentLine, [scrollToCurrentLine]);

One solution can be to define setCurrentLineIndex such that it receives a callback.

//assuming the parent container is a class
setCurrentLineIndex = (index, cb)=>{
    //write the logic here then execute the callback 
    cb()
}

// Then in your child component, you could do something like this 
    setCurrentLineIndex(nextIndex, scrollToCurrentLine)

I solved a similar problem by creating a little npm module. It allows you to subscribe to and listen for redux actions and executes the provided callback function as soon as the state change is complete. Usage is as follows. In your componentWillMount or componentDidMount hook:

 subscribeToWatcher(this,[
      {  
        action:"SOME_ACTION",
        callback:()=>{
          console.log("Callback Working");
        },
        onStateChange:true
      },
    ]);

Detailed documentation can be found at this link

Related