1

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 });

opensorcio
  • 59
  • 2
  • 6
  • 2
    "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." To me this is the right solution - state should only consist of things that should cause a rerender when they change, and you should use refs for other persistent values. And I don't understand your difficulty with doing this - so could you provide more details? – Robin Zigmond Apr 24 '22 at 12:23
  • @RobinZigmond It's just that in my state I have three refs and calling functions with four arguments when I could call them with a single one (the state) is ugly. Also they really are semantically related. And as a bonus, by putting them in the state I can avoid accessing `.current` every time. – opensorcio Apr 24 '22 at 12:28
  • 1
    well you don't show the functions themselves, or explain how these variables are "semantically related". It's hard to give concrete comments without seeing code - but it sounds like you could write some sort of update function that will either update the state or the appropriate ref, depending on what it is passed? – Robin Zigmond Apr 24 '22 at 12:31
  • @opensorcio - Are `c` and `d` used for rendering (directly or indirectly)? – T.J. Crowder Apr 24 '22 at 12:31
  • @RobinZigmond yes I could create such function, it would be a wrapper around the state reducer, but if it is safe to have the refs in the state, why not simplify things? – opensorcio Apr 24 '22 at 12:35
  • @T.J.Crowder I don't think, `a` and `b` are used for rerendering, `c` and `d` may be for example an HTMLElement on which I use `.focus()` – opensorcio Apr 24 '22 at 12:37
  • because regardless of whether it's "safe" or not (I confess I don't know for sure), it's not "semantically" correct. State is for values needed for rendering, if you need a persistent value that's not related to rendering, refs are the appropriate thing to use. – Robin Zigmond Apr 24 '22 at 12:38
  • @opensorcio - *"...`c` and `d` may be for example an HTMLElement."* That sounds suspicious. How are you getting those elements? Via refs I assume? Then you're putting ref information into state? That does not sound like a good idea, but it's hard to tell without more context. – T.J. Crowder Apr 24 '22 at 12:40
  • 1
    FWIW, I wholeheartedly agree with @RobinZigmond. If it's not supposed to trigger a re-render if it changes, and especially if it's not used for rendering, then it shouldn't be in state. Whether you can get away with it or not, it's (as they said) semantically incorrect, and surprising to other programmers, so it's a maintenance hazard. – T.J. Crowder Apr 24 '22 at 12:42
  • 2
    @T.J.Crowder I get the elements from `document.querySelector()` (I am writing a focus trap). All right so thank you both Robin and T.J. for your opinion, I guess that using actual refs is the right thing to do, and that's what I am going to do. It would be interesting to know the answer to the question though. – opensorcio Apr 24 '22 at 12:57

1 Answers1

2

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. :-)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • So assuming the changes are not involved in rerenders, it is safe to apply them directly. It's *only* highly discouraged to do so because it may be misleading on how the code works. I don't have enough reputation to cast an upvote lol I will come back in the future. – opensorcio Apr 24 '22 at 13:15
  • 1
    @opensorcio - Yes, that's right. (Don't worry about coming back later. :-) It's all good.) – T.J. Crowder Apr 24 '22 at 13:16
  • 1
    @opensorcio - I added a bit of a tangent to the end of the answer. Happy coding! – T.J. Crowder Apr 24 '22 at 13:20
  • 1
    Thanks for the tangent :) I used to create a ref for each object. – opensorcio Apr 24 '22 at 14:05