Home > Enterprise >  How should I trigger a re-render when the state is updated inside useEffect()
How should I trigger a re-render when the state is updated inside useEffect()

Time:11-29

I'm trying to figure out how I should structure my code with React hooks such that:

  1. I have a state variable that represents whether or not my app is "loading" results
  2. This loading state is set to true when useEffect() runs to fetch data to display
  3. At the end of useEffect() I set loading is set to false
  4. In child components, this loading state is passed as a prop to change various components

A simplified model of my code is below:

const App = () => {
   const [state, stateDispatch] = useReducer(stateReducer,{loading: false})
   useEffect(() => {
      stateDispatch({type:'loadingResults'});
      loadResults();
      stateDispatch({type:'finishedLoadingResults'});
   });
   return (
      <ExampleComponent loading={state.loading} />
   );
}

const stateReducer = (prevState,action) => {
    switch(action.type) {
      case 'loadingResults':
        return {...prevState, loading:true};
      case 'finishedLoadingResults':
        return {...prevState, loading: false};
      default:
        throw new Error();
    }

}

The example "child" component is below:

const SampleComponent = (props) => {
    const [buttonText, setButtonText] = useState('Load More');

    useEffect(() => {
        if (props.loading) {
            setButtonText('Loading...')
        } else {
            setButtonText('Load More');
        }
    },[props.loading]);

    return (
        <div onClick={(event) => props.getBooks(event,false)} className="moreResults">{buttonText}</div>
    )
}

With this current setup, I never see the child component re-rendered - even though it appears that the if (props.loading) is indeed evaluated correctly. For example, if I put a console.log inside that if check and the else check, I see both print out when the loading state is toggled.

Alternatively, if I define state.loading has a dependency for the useEffect() function in the App component, I get infinite re-renders since I'm toggling the loading state inside useEffect().

Any idea what I'm missing here?

CodePudding user response:

You don't need additional useEffect or useState in your child component. The prop is changing in the parent, so that's enough to rerender your component.

const SampleComponent = (props) => (
  <div onClick={(event) => props.getBooks(event,false)}
   className="moreResults">{props.loading ? 'Loading...' : 'Load More'}
  </div>
)

CodePudding user response:

  • First of all, the useEffect in your App component has no dependency list, so it runs on every render. Since it sets the state, this causes a new re-render, after which your useEffect gets called again, and so on.. So you get an infinite loop.

    You must add a dependency list to the useEffect.

  • Secondly, If your loadData function is an asynchronous function (which it should be if you're fetching data), it will return a promise, so you can dispatch "finishedLoadingResults" when the promise resolves.

    This gives the following for your useEffect:

    useEffect(() => {
        stateDispatch({ type: "loadingResults" });
        loadResults()
            .then(() => stateDispatch({ type: "finishedLoadingResults" }));
    }, []);
  • Third, as jmargolisvt pointed out already, you don't need an extra useEffect inside your child component. When your App component sets the "loading" state, this will cause the App component together with the child component to re-render, so no need to use the buttonText state variable.

You can see the working code in this Sandbox.

CodePudding user response:

A component decides to re-render only when any of its prop or state changes.

Javascript runs everything synchronously because of its single threaded nature. So the react diffing function to identify whether the component needs to be re-rendered or not will happen in synchronous order which is after the useEffect hook execution completes in this case.

useEffect(() => {
    stateDispatch({type:'loadingResults'}); 
    // -> state update happened
    // -> but re-render won't happen, since useEffect metho execution is not complete yet
    loadResults();
    stateDispatch({type:'finishedLoadingResults'});
 });

But by then you have finished loading and reset the state. Since there is no change, the component won't re-render.

Also you cannot set the state outside the hook because, any setState triggers will trigger a re-render which will again set the state and this will create an infinite loop.

Solution: Call the setState for loading inside an if condition outside of the useEffect block.Something like this.

const App = () => {
   const [state, stateDispatch] = useReducer(stateReducer,{loading: false})
   useEffect(() => {
      if(!state.loading) {
          loadResults();
          stateDispatch({type:'finishedLoadingResults'});
      }
   },[state.loading]);
   if(notResults) {
      stateDispatch({type:'loadingResults'});
   }
   return (
      <ExampleComponent loading={state.loading} />
   );
}
  • Related