infinite loop when querying api in redux action

Viewed 2705

I am attempting to query my Firebase backend through a redux-thunk action, however, when I do so in my initial render using useEffect(), I end up with this error:

Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.

My action simply returns a Firebase query snapshot which I then received in my reducer. I use a hook to dispatch my action:

export const useAnswersState = () => {
    return {
        answers: useSelector(state => selectAnswers(state)),
        isAnswersLoading: useSelector(state => selectAnswersLoading(state))
    }
}

export const useAnswersDispatch = () => {
    const dispatch = useDispatch()
    return {
        // getAnswersData is a redux-thunk action that returns a firebase snapshot
        setAnswers: questionID => dispatch(getAnswersData(questionID))
    }
}

and the following selectors to get the data I need from my snapshot and redux states:

export const selectAnswers = state => {
    const { snapshot } = state.root.answers
    if (snapshot === null) return []
    let answers = []
    snapshot.docs.map(doc => {
        answers.push(doc.data())
    })
    return answers
}

export const selectAnswersLoading = state => {
    return state.root.answers.queryLoading || state.root.answers.snapshot === null
}

In my actual component, I then attempt to first query my backend by dispatching my action, and then I try reading the resulting data once the data is loaded as follows:

const params = useParams() // params.id is just an ID string

const { setAnswers, isAnswersLoading } = useAnswersDispatch()
const { answers } = useAnswersState()

useEffect(() => {
    setAnswers(params.id)
}, [])

if (!isAnswersLoading)) console.log(answers)

So to clarify, I am using my useAnswersDispatch to dispatch a redux-thunk action which returns a firebase data snapshot. I then use my useAnswersState hook to access the data once it is loaded. I am trying to dispatch my query in the useEffect of my actual view component, and then display the data using my state hook.

However, when I attempt to print the value of answers, I get the error from above. I would greatly appreciate any help and would be happy to provide any more information if that would help at all, however, I have tested my reducer and the action itself, both of which are working as expected so I believe the problem lies in the files described above.

4 Answers

Try refactoring your action creator so that dispatch is called within the effect. You need to make dispatch dependent on the effect firing.

See related

const setAnswers = (params.id) => {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(useAnswersDispatch(params.id));
  }, [])
}

AssuminggetAnswersData is a selector, the effect will trigger dispatch to your application state, and when you get your response back, your selector getAnswersData selects the fields you want.

I'm not sure where params.id is coming from, but your component is dependent on it to determine an answer from the application state.

After you trigger your dispatch, only the application state is updated, but not the component state. Setting a variable with useDispatch, you have variable reference to the dispatch function of your redux store in the lifecycle of the component.

To answer your question, if you want it to handle multiple dispatches, add params.id and dispatch into the dependencies array in your effect.

// Handle null or undefined param.id
const answers = (param.id) => getAnswersData(param.id);
const dispatch = useDispatch();
useEffect(() => {
     if(params.id) 
        dispatch(useAnswersDispatch(params.id));
  }, [params.id, dispatch]);

console.log(answers);

As commented; I think your actual code that infinite loops has a dependency on setAnswers. In your question you forgot to add this dependency but code below shows how you can prevent setAnswers to change and cause an infinite loop:

const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
  const { type, payload } = action;
  console.log('in reducer', type, payload);
  if (type === GOT_DATA) {
    return { ...state, data: payload };
  }
  return state;
};

//I guess you imported this and this won't change so
//   useCallback doesn't see it as a dependency
const getAnswersData = id => ({
  type: GOT_DATA,
  payload: id,
});

const useAnswersDispatch = dispatch => {
  // const dispatch = useDispatch(); //react-redux useDispatch will never change
  //never re create setAnswers because it causes the
  //  effect to run again since it is a dependency of your effect
  const setAnswers = React.useCallback(
    questionID => dispatch(getAnswersData(questionID)),
    //your linter may complain because it doesn't know
    //  useDispatch always returns the same dispatch function
    [dispatch]
  );
  return {
    setAnswers,
  };
};

const Data = ({ id }) => {
  //fake redux
  const [state, dispatch] = React.useReducer(reducer, {
    data: [],
  });

  const { setAnswers } = useAnswersDispatch(dispatch);
  React.useEffect(() => {
    setAnswers(id);
  }, [id, setAnswers]);
  return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
  const [id, setId] = React.useState(88);
  return (
    <div>
      <button onClick={() => setId(id => id + 1)}>
        increase id
      </button>
      <Data id={id} />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Here is your original code causing infinite loop because setAnswers keeps changing.

const GOT_DATA = 'GOT_DATA';
const reducer = (state, action) => {
  const { type, payload } = action;
  console.log('in reducer', type, payload);
  if (type === GOT_DATA) {
    return { ...state, data: payload };
  }
  return state;
};

//I guess you imported this and this won't change so
//   useCallback doesn't see it as a dependency
const getAnswersData = id => ({
  type: GOT_DATA,
  payload: id,
});

const useAnswersDispatch = dispatch => {
  return {
    //re creating setAnswers, calling this will cause
    //  state.data to be set causing Data to re render
    //  and because setAnser has changed it'll cause the
    //  effect to re run and setAnswers to be called ...
    setAnswers: questionID =>
      dispatch(getAnswersData(questionID)),
  };
};
let timesRedered = 0;
const Data = ({ id }) => {
  //fake redux
  const [state, dispatch] = React.useReducer(reducer, {
    data: [],
  });
  //securit to prevent infinite loop
  timesRedered++;
  if (timesRedered > 20) {
    throw new Error('infinite loop');
  }
  const { setAnswers } = useAnswersDispatch(dispatch);
  React.useEffect(() => {
    setAnswers(id);
  }, [id, setAnswers]);
  return <pre>{JSON.stringify(state.data)}</pre>;
};
const App = () => {
  const [id, setId] = React.useState(88);
  return (
    <div>
      <button onClick={() => setId(id => id + 1)}>
        increase id
      </button>
      <Data id={id} />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

You just need to add params.id as a dependency.

Don't dispatch inside the function which you are calling inside useEffect but call another useEffect to dispatch

 const [yourData, setyourData] = useState({});

  useEffect(() => {
    GetYourData();
  }, []);

  useEffect(() => {
    if (yourData) {
      //call dispatch action 
      dispatch(setDatatoRedux(yourData));
    }
  }, [yourData]);


  const GetYourData= () => {
    fetch('https://reactnative.dev/movies.json')
       .then((response) => response.json())
       .then((json) => {
            if (result?.success == 1) {
              setyourData(result);
            }
       })
    .catch((error) => {
      console.error(error);
    });
  };
Related