Exceeding update depth with userReducer but not with useState

Viewed 24

I have an useEffect that triggers every time an user types something in the input. At first I had it running with an useState and it was working smooth as butter, but I was required to change it to useReducer. The useEffect with useReducer:

useEffect(() => {
dispatch({type : 'username', payload : watch().username});
dispatch({type : 'password', payload : watch().password});
console.log(state.username, state.password);
}, [watch()]);

The useEffect with useState :

useEffect(() => {
setPassword(watch().password);
setUsername(watch().username);
console.log(username, password);
}, [watch()]);

And the useEffect constantly is being triggered even when the users is not typing anything, as when used with useState it does not do that.

The reducer :

 const reducer = (state, action) => {
switch (action.type) {
  case "username":
    return {...state, username: action.payload };
  case "password":
    return {...state, password : action.payload};
}
};
1 Answers

There's a few red flags here, but the main one that stands out is how you're using watch(). useEffect and useState will (likely) be re-running on every render because watch() is probably returning a non-memoized object. The reason your code "works" using useState is because setting a state object to the same thing does nothing - if the username or password isn't changing, the state doesn't get updated, so your component doesn't re-render.

This isn't true with useReducer though, unless you explicitly cater for it in your reducer function - something like this will probably fix the bug:

const reducer = (state, action) => {
  switch (action.type) {
    case "username":
      return state.username !== action.payload
        ? {...state, username: action.payload }
        : state;

    case "password":
      return state.password !== action.payload
        ? {...state, password: action.payload }
        : state;

    default: 
      return state;
  }
};

However, you should also try and make the dependencies for hooks as simple as possible. Is there any reason you couldn't call watch() first, and then pass the values to your effect?

const { username, password } = watch();

useEffect(() => {
  dispatch({type : 'username', payload : username});
  dispatch({type : 'password', payload : password});
}, [username, password]);

Or, you could go one better and separate the two into two different effects - there's no need to dispatch a username update if the password has changed:

const { username, password } = watch();

useEffect(() => {
  dispatch({type : 'username', payload : username});
}, [username]);

useEffect(() => {
  dispatch({type : 'password', payload : password});
}, [password]);

And, while we're in refactoring mode, why not create a custom hook to handle the logic for you? Something like:

const useLoginFormState = (formValues) => {
  const { username, password } = formValues;

  useEffect(() => {
    dispatch({type : 'username', payload : username});
  }, [username]);

  useEffect(() => {
    dispatch({type : 'password', payload : password});
  }, [password]);

  return formValues;
}

// Now in your component -
const MyComponent = () => {
  const formValues = useLoginFormState(watch());
};

Related