8

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.

mlz7
  • 2,067
  • 3
  • 27
  • 51
  • 2
    Hey, unless you depend on `setAnswers, isAnswersLoading, answers` to trigger an action which changes one of these, I don't see how you get into an infinite loop. Can you create a small codesandbox or something to demonstrate your issue – Shubham Khatri Mar 22 '20 at 14:38
  • You could however, try to execute your hooks outside of return `export const useAnswersState = () => { const answers = useSelector(state => selectAnswers(state)); const isAnswersLoading = useSelector(state => selectAnswersLoading(state)); return { answers, isAnswersLoading } }` – Shubham Khatri Mar 22 '20 at 14:38
  • There should be more code to cause the infinite loop, you can make a minimal codesandbox if you are looking for an answer. – think-serious Mar 27 '20 at 15:10

4 Answers4

0

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>
HMR
  • 37,593
  • 24
  • 91
  • 160
0

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

10 Rep
  • 2,217
  • 7
  • 19
  • 33
0

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);
    });
  };
-1

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);
laujonat
  • 342
  • 1
  • 8
  • hmm ok so this is not really what I'm asking, I'm trying to create a hook for dispatching my `getAnswersData` action which returns a data snapshot, it is not a selector but rather an api call. I then use the `useAnswersState` hook to actually access the data. I apologize if this wasn't clear – mlz7 Mar 19 '20 at 05:06