Home > Blockchain >  Is it safe to modify a react state directly, if you don't need a rerender?
Is it safe to modify a react state directly, if you don't need a rerender?

Time:04-25

It looks like modifying a react state directly is a bad practice (Do Not Modify State Directly) and the reasons have already been discussed many times on SO (see a question) and outside in the web (see a blog post).

The problem seems to be that when the state is modified directly, changes are made to the state in the current virtual DOM. So when it's time for a rerender to occur, the state in the new virtual DOM would be the same as the state in the previous one and the actual DOM would not be updated.

But what if you don't want to trigger a rerender when modifying the state? In this case It should be safe to modify the state directly, isn't it?

Below is described the situation I am facing, which brought me into the rabbit hole of assessing whether I can safely modify the state directly, under certain circumstances.

I have a react useReducer state consisting of an object with many keys.

const initialState = {
  a: 0,
  b: 1,
  c: 2,
  d: 3,
};

In the state reducer I don't always return a new state. Say that if I want to modify a or b then I also want to trigger a rerender, but if I modify c or d I don't want because the DOM is not being affected anyway.
I think of c and d as sort of refs: they are mutable within a single render, yet their value is preserved between rerenders.

I could, of course, limit the state to only a and b and create a ref for each of c and d, but they are all related and I often need to pass them all together to a function. So in my case it works better to keep the refs in the state.
Here's how the reducer would look like:

// When modifying `a` or `b` `action.modifyRef` is set to `false`.
// When modifying `c` or `d` `action.modifyRef` is set to `true`.

const reducer = (state, action) => {
  if (action.modifyRef) {
    state[action.key] = action.value;
    return state;
  }
  const newState = {
    ...state,
    [action.key]: action.value,
  };
  return newState;
};

Am I right to believe I can safely modify the state without triggering a rerender, if a rerender is not desired in the first place?
If the answer to the previus question is "yes", then to modify c or d I can just modify the state directly instead of dispatching an action against the reducer, can't I?

state.c = 5;

// the above statement is equivalent to the one below

dispatchState({ modifyRef: true, key: 'c', value: 5 });

CodePudding user response:

I don't think the c and d you're describing (members that shouldn't cause re-rendering, and which are not used for rendering) are state information as the term is used in React. They're instance information. The normal way to use non-state instance information in function components is to use a ref.

On a pragmatic bits-and-bytes level, can you hold non-state information in state and modify it directly instead of using a state setter (directly or indirectly)? Yes, you probably can, at least initially. The only scenarios where I can see that causing incorrect behavior of the app/page involve rendering, and you've said they aren't used for that.

But if you do:

  • It'll be confusing for other team members (or you yourself, if you have to come back to the code after a break). Semantics matter. If you call it state, but it's not state, that's going to trip someone up. It's like calling something a function that isn't a function: at some point, someone's going to try to call that "function."
  • It'll be a maintenance hazard, because a team member (or you yourself after a break) may make an innocuous change such that c or d are used for rendering (because after all, they're in state, so it's fine to use them for rendering), perhaps by passing one of them as a prop to a child component. Then you're in the situation where the app won't rerender properly when they change.

A slight tangent, but in a comment on the question you mentioned that you "...they are all related and I often need to pass them all together to a function..."

Using a ref to hold c and d, the set up might look like this:

const [{a, b}, dispatch] = useReducer(/*...*/);
const instanceRef = useRef(null);
const {c, d} = instanceRef.current = instanceRef.current ?? {c: /*...*/, d: /*...*/};

Then getting an object in order to treat them as a unit is:

const stuff = {a, b, c, d};
// ...use `stuff` where needed...

Creating objects is very in expensive in modern JavaScript engines, since it's a common operation they aggressively optimize. :-)

  • Related