-2

Supposed that I have a complex state with tones of fields that are expensive to copy which I want to update in a certain order. The "supposedly" correctly way is probably

setState({
  ...myComplexState,
  expensiveFieldA: newA,
});

// some logic to compute newB

setState({
  ...myComplexState,
  expensiveFieldB: newB,
});

This not only triggers unnecessary re-renders but also wastes cpu cycles on coping unchanged fields, making the alternative pattern appealing

import { useState } from 'react';

class StateWrapper<S> {
  state: S;

  constructor(state: S) {
    this.state = state;
  }

  shallowCopy() {
    return new StateWrapper(this.state);
  }
}

function useObjState<T>(initState: T): [T, () => void] {
  const [wrapper, setWrapper] = useState(() => new StateWrapper(initState));
  const commit = () => setWrapper(wrapper.shallowCopy());

  return [wrapper.state, commit];
}

class ExpensiveState {
  private val: string;

  constructor() {
    this.val = '';
  }

  preformExpensiveOperations(val: string) {
    this.val = val;
  }

  preformAnotherExpensiveOperation() {}

  getVal() {
    return this.val;
  }
}

function App() {
  const [state, commit] = useObjState(new ExpensiveState());

  return (
    <>
      <p>val: {state.getVal()}</p>
      <p>
        <input
          onChange={e => {
            state.preformExpensiveOperations(e.target.value);
            state.preformAnotherExpensiveOperation();
            commit();
          }}
        />
      </p>
    </>
  );
}

export default App;

I know it's anti-pattern but why is it discouraged?

  • 2
    You should use ref instead of state if you want to trigger rerender manually and mutate the object. State it reserved for variables that should trigger rerender on change – Konrad Apr 08 '23 at 21:29
  • You're basically building your own state management system parallel to `useState` and then just using the `setWrapper` call to force a render when it suits. This allows the state object and the UI to drift out of sync, not to mention that now every element in state has to be a class instance of it's own with it's own value access. It also bypasses all of the efficencies that React's state management offers ie. batching of `setState` calls, accessing prior state values, storing prior states as history, bailing out of unneccesary renders etc. – pilchard Apr 08 '23 at 21:48
  • Also, as noted by Konrad, you could just be using a `ref` with a dummy state that you call to force a render (I'm not recommending it). – pilchard Apr 08 '23 at 21:49
  • Does this answer your question? [ReactJS: Why shouldn't I mutate nested state?](https://stackoverflow.com/questions/32125063/reactjs-why-shouldnt-i-mutate-nested-state) – pilchard Apr 08 '23 at 22:17
  • more complete duplicate: [Why can't I directly modify a component's state, really?](https://stackoverflow.com/questions/37755997/why-cant-i-directly-modify-a-components-state-really) – pilchard Apr 08 '23 at 22:18
  • I'm not sure if React does a deep comparison between the old and the new state. As far as I know, React considers two states to be different if they reference different objects. `arr.push` & `x.y.z = newZ` are discouraged because `arr` & `x` still reference the same object. What I did was different: `StateWrapper::shallowCopy` creates a new object, leaving it no room for React to confuse it with the old state. According to React's documentation, `useRef` is a React Hook that lets you reference a value that’s not needed for rendering. My understanding is that `ref` are meant for non-states – Winston Moxley Apr 08 '23 at 23:08
  • React makes a shallow comparison using `Object.is` see: [React: useState](https://react.dev/reference/react/useState#setstate-caveats). So yes, you are creating a new object to force a render. `ref` is simply a hook that allows persisting values through renders without causing renders, so as i mentioned you could just be storing all your 'state' (you aren't really using `useState` as state here anyway, just as a means of persisting values through renders) in a ref and then call some dummy state with a new empty object. Again I don't recommend this, but that is essentially what you are doing. – pilchard Apr 08 '23 at 23:15
  • Here is a quick [sandbox](https://codesandbox.io/s/ref-as-state-ck5ry1) illustrating the `ref` example described above. It also clearly illustrates the most basic pitfall which is state/ui sync issues. – pilchard Apr 08 '23 at 23:25
  • @pilchard thanks for the sandbox. Similar to how I wrap `State` inside `StateWrapper`, `ref` wraps `current` inside it. Since you don't recommend using `ref` for this (which I agree), what would you recommend? – Winston Moxley Apr 09 '23 at 00:04
  • I would recommend using `useState` as it is intended. You can also look at libraries such as [Redux](https://redux.js.org/) which will have taken everything you seem to be concerned about into account for complex state management, or immutable libraries such as [ImmutableJs](https://immutable-js.com/) or [Immer](https://immerjs.github.io/immer/) which efficiently handle immutability in preparation for passing to `setState` – pilchard Apr 09 '23 at 00:21

1 Answers1

0

The most obvious reason it's discouraged is because it goes against the principles of React, which emphasize writing code in a clear and declarative way.

The less obvious reason is that while your alternative pattern does look like it could work, it could wind up causing unexpected behavior. And if someone is debugging your code long after you're gone, they might not know the best way to solve it.

So for posterity's sake, we rely on setState, but if you're really after performance then I recommend using Redux to handle complex state.

ingenium21
  • 55
  • 1
  • 10