React infinite loop dispatch, from useReducer hook

Viewed 1385

The main logic of the component is that when the page is loaded, need to save the parameters from the URL (token) and pass them from the dispatch() function. To do this, I have a state value were initially null is stored, but after rendering the component I add a token.

component.js

const [state, dispatch] = useReducer()
const [value, setValue] = useState(null)

  useEffect(() => {
    const query = window.location.search || ''
    const queryString = new URLSearchParams(query || '')
    const token = queryString.get('token')
    setValue(token)
  }, [value])

   useEffect(() => {
    if (value && value !== null) {
      // console.log(1)
      dispatch({
        type: 'TEST_TYPE',
        payload: { data: value },
      })
    }
  }, [value])

When I want to call the dispatch() function in useEffect() the component is reloaded until the memory is full. I tested the second method, called the dispatch() function into useCallback() to call once if there are new changes. But the dispatch function is not called

const dis = useCallback(() => dispatch, [])

  useEffect(() => {
    const query = window.location.search || ''
    const queryString = new URLSearchParams(query || '')
    const token = queryString.get('token')
    if (token) {
      dis({
         type: 'TEST_TYPE',
         payload: { data: token },
      })
    }
  }, [dis])

How can I properly make dispatch work once when the component is loaded and without an infinite loop?

3 Answers

You said that : I have a state value where initially null is stored, but after rendering the component I add a token. if that is the case then you are doing it incorrectly .Instead of:

useEffect(() => {
    const query = window.location.search || ''
    const queryString = new URLSearchParams(query || '')
    const token = queryString.get('token')
    setValue(token)
  }, [value])

it should be:

useEffect(() => {
    const query = window.location.search || ''
    const queryString = new URLSearchParams(query || '')
    const token = queryString.get('token')
    setValue(token)
  }, [])

Now as soon as your component renders ,value will get updated. Now after this the below useEffect will come into picture:

useEffect(() => {
    if (value && value !== null) {
      // console.log(1)
      dispatch({
        type: 'TEST_TYPE',
        payload: { data: value },
      })
    }
  }, [value])

and dispatch will be called.

When using useReducer hook you have to specify the reducer function and the initial state.

function reducer(state, action) {
  return action.payload;
}

function YourComponent() {
  const [state, dispatch] = useReducer(reducer, { data: null });

...

Then you can just utilize your state from useReducer instead of storing duplicated data in both state from useReducer and state from useState

  useEffect(() => {
    const query = window.location.search || "";
    const queryString = new URLSearchParams(query || "");
    const token = queryString.get("token");

    if (token) {
      dispatch({
        type: "TEST_TYPE",
        payload: { data: token },
      });
    }
  }, []);

So to sum up, your code should resemble the following

import React, { useReducer, useEffect } from "react";

function reducer(state, action) {
  return action.payload;
}

export default function YourComponent() {
  const [state, dispatch] = useReducer(reducer, { data: null });

  useEffect(() => {
    const query = window.location.search || "";
    const queryString = new URLSearchParams(query || "");
    const token = queryString.get("token");

    if (token) {
      dispatch({
        type: "TEST_TYPE",
        payload: { data: token },
      });
    }
  }, []);

  return (
    <div>
      <pre>{JSON.stringify(state)}</pre>
    </div>
  );
}

Just like Rostyslav said, you're not using useReducer in the right way. This is different from the useState hook: in that one, the parameter that it receives will serve as a default value for the defined variable within the component' state, which is why it's possible to leave it blank. When you're going to modify its contents, you just need to update its value by using the setter function returned by the hook.

However, useReducer requires that you explicitly define the way in which the data will change according to certain actions of your choice. It's necessary for this method to receive at least two parameters: the reducer function and an initial state which will be the starting point for upcoming data transformations.

The aim of the reducer function is to perform changes on the state in response to the dispatched actions. This method will receive the current state and an action object (which is the value that you're sending via dispatch). Depending on the received action, the reducer method must then apply some transformations in the data and return a new object that will override the current state.

The following piece of code illustrates a reducer that would work in your case. By default, the token property of the state will be set to null. As you can see in the code of the reducer, when an action of type UPDATE_TOKEN is triggered, the token property of your state will be updated to contain the token bundled into the action payload.

const initialState = {
  token: null
};

const reducer = (state, action) => {
  switch (action.type) {
    case "UPDATE_TOKEN":
      return {
        ...state,
        token: action.payload.token
      };
    default:
      return state;
  }
};

Since you'll be storing the token into the store by dispatching an action, there's no need for you to use useState to save it into a value property beforehand. If you need to access the token from anywhere in your component, you can retrieve it directly from your appState variable, as in the code below:

const App = () => {
  const [appState, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    const query = window.location.search || "";
    const queryString = new URLSearchParams(query || "");
    const token = queryString.get("token");

    if (token) {
      dispatch({
        type: "UPDATE_TOKEN",
        payload: { token }
      });
    }
  }, []);

  return <div className="value">Token: {appState.token}</div>;
};

The full working example can be found here.

Even though this should work, you may have to reconsider if you really need useReducer or not. Adding reducers and actions adds complexity to the code, so it only make sense for larger data structures where more logic is required to update them. In this particular example it doesn't seems worth the effort, as the only action you need to perform is to change the value of the token variable: since this variable is merely a string, this can be easily achieved by just using useState.

Related