react icon indicating copy to clipboard operation
react copied to clipboard

Bug: when initialArg changes, useReducer should update the state accordingly

Open tavoyne opened this issue 4 years ago • 6 comments

TL;DR: After the first render, useReducer doesn't react to changes in the initialArg (second positional) argument. In my opinion, it should update the state accordingly. The actual behaviour in unnecessarily restrictive and forces us to rely on hacks to address the problems it brings.

Let's take the example of a form provider, a component that enables us to make javascript objects easily editable by users through inputs:

// App.js

const users = {
  1: {
    firstName: 'Paul',
    lastName: 'Atreides',
  },
  2: {
    firstName: 'Duncan',
    lastName: 'Idaho',
  },
};

const App = () => {
  const [id, setId] = useState(1);

  return (
    <>
      <div>Pick User</div>
      <button onClick={() => { setId(1); }} type="button">User 1</button>
      <button onClick={() => { setId(2); }} type="button">User 2</button>
      <FormProvider initialValues={users[id]}>
        <Editor />
      </FormProvider>
    </>
  );
};
// FormProvider.js

const reducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value };
    default:
      throw new Error();
  }
};

const FormProvider = ({ children, initialValues }) => {
  const [values, dispatch] = useReducer(reducer, initialValues);

  const handleChange = useCallback((evt) => {
    dispatch({
      field: evt.target.name,
      type: 'UPDATE_FIELD',
      value: evt.target.value,
    });
  }, []);

  return (
    <FormContext.Provider value={{ handleChange, values }}>
      {children}
    </FormContext.Provider>
  );
};
// Editor.js

const Editor = () => {
  const { handleChange, values } = useContext(FormContext);

  return (
    <>
      <div>First name:</div>
      <input
        name="firstName"
        onChange={handleChange}
        value={values.firstName}
      />
      <div>First name:</div>
      <input
        name="lastName"
        onChange={handleChange}
        value={values.lastName}
      />
    </>
  );
};

React version: 17.0.2

Steps To Reproduce

  1. Click on the "User 2" button.
  2. Notice nothing happens.

Link to code example: https://codesandbox.io/s/wandering-cache-4vwwe?file=/src/App.js.

The current behavior

When clicking on the "User 2" button, nothing happens. As far as I understand, this is by design. I believe you guys were afraid that people would forget to keep the identity of initialArg stable, which would lead to the state being accidentally reset on every cycle. This does make sense, however it makes composition more difficult for developers who know what they are doing and who do want the state to be updated whenever initialArg changes.

Today, here are my two options:

1. Adding a key prop to <FormProvider />

<FormProvider key={id} initialValues={users[id]}>

This will cause the entire component (and its children) to be unmounted/remounted. The useReducer hook will thus initialise from scratch, with the correct initialValues. Whereas this does work, it's pretty bad in terms of performance and I mean ... it's an ugly hack.

2. Dispatch a RESET action whenever initialValues changes

// FormProvider.js

const reducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value };
    case 'RESET':
      return action.values;
    default:
      throw new Error();
  }
};
const FormProvider = ({ children, initialValues }) => {
  // ...

  const isFirstRenderRef = useRef(true);

  useEffect(() => {
    if (!isFirstRenderRef.current) {
      dispatch({
        type: 'RESET',
        values: initialValues,
      });
    }
  }, [initialValues]);

  useEffect(() => {
    isFirstRenderRef.current = false;
  }, []);

 // ...
};

This will indeed trigger the updating of the state. However, it won't happen before the next cycle. It means that there'll be a moment where the <FormProvider> state will not be mirroring the user's selected profile. Should he decide to update a field during this fraction of a second and it will be an absolute, unforgettable disaster.

The expected behavior

This problem wouldn't exist if the useReducer state was updating when changes in initialArg occur. This is also the most natural behaviour. It would do no harm to developers who want the state not to be updated even though initialArg changes (god, who are these guys ?!), as they might use useState, useRef (useReducer(reducer, useRef(initialValues).current), useMemo or just move the variable outside of the component).

tavoyne avatar Nov 06 '21 16:11 tavoyne

Hey nice write up.

MauricioAndrades avatar Nov 10 '21 08:11 MauricioAndrades

I can confirm I am doing something very similar and it took me a week to figure out this was a framework issue. Would appreciate if this was addressed.

tvillafane avatar Apr 18 '23 18:04 tvillafane

I have a similar form. When fetching the initial data from server using RTK Query , initialArg does not updates.

Nahiek avatar Oct 10 '23 12:10 Nahiek

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

github-actions[bot] avatar Apr 10 '24 10:04 github-actions[bot]

I still have this issue. I end up updating the key of the parent component every time the initial data changes

Nahiek avatar Apr 17 '24 14:04 Nahiek

@tavoyne How did you solve this issue?

Nahiek avatar Apr 17 '24 14:04 Nahiek

This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!

github-actions[bot] avatar Jul 16 '24 18:07 github-actions[bot]

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!

github-actions[bot] avatar Jul 23 '24 19:07 github-actions[bot]