0

I am a C++ developer and new to React and JS. I somehow understand the concept of React and the reason it requires to have an immutable state:

  1. because it has to compare existing state on setVar (form useState) with a new state. Otherwise it will have no reason to rerender.
  2. Because of the possible data races on shared resources (this is a problem in general in asynchronous programming, not just with React).

I also know, that there are common practices (looking throughout the history of React, I'd say they are still evolving) you have to stick to in order to work with other people in order to understand each other. But still, I really don't like the concept of copying data just for the sake of updating it:

  • it feels like a lot of overhead for copying large arrays of data
  • you have to come up with a rather complicated code to modify at least one element in an array. With trees it is even harder.

So, given the following assumptions, I want to ask if it might be alright to use a workaround to modify data in place:

  • the data is not shared between other react components
  • only one function is allowed to modify it
  • the component will not read the data while it is being modified

These are just assumptions and maybe it is not possible to enforce in a real world application.

const Counter = () => {

    const [clicks, setClicks] = useState({value: 0});
    const [, setUpdateRequired] = useState({});

    const increment = () => {
        clicks.value -= 1;
        // setClicks({value: clicks.value - 1}); // this is a proper way 
        setUpdateRequired({});
    }

    return (
        <div>
            <p>
                Hi, my name is {name}.
                You have clicked {clicks.value} times.
            </p>
            <p>
                <button style={{
                    backgroundColor: 'gray',
                    border: '1px solid black'
                }} onClick={increment}>Click Me!
                </button>
            </p>
        </div>
    )
}

Is this really that bad?


I expected the question to be downvoted. But: I was looking for an explanation to thoroughly understand what could go wrong with this approach. I didn't find a question with regard to the subject as I put it: Usually people ask why their components does not rerender when they modify state. The answer is - you must set state via a hook. Fair point. In my case I am confused: the hook I use to rerender component does not modify the counter value. It modifies a dummy object. I thought that react could evaluate what needs to be updated and only partly rerender the component. But in that case I would not see an incremented counter each time I press a button.

I did mention in a question that it is most probably a very bad approach, but I fail to see the reason, so I would like to receive an explanation with examples.


This is not about asking for an opinion if it is bad. I ask WHY is it bad.

Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28

2 Answers2

2

If you know what and why are you doing, you can do it. In your artificial example, the official React way is just a way cleaner, and efficient, thus that's the way to go

const Counter = () => {

    const [clicks, setClicks] = useState(0);

    return (
        <div>
            <p>
                Hi, my name is {name}.
                You have clicked {clicks} times.
            </p>
            <p>
                <button style={{
                    backgroundColor: 'gray',
                    border: '1px solid black'
                }} onClick={() => setClicks(1 + clicks)}>Click Me!
                </button>
            </p>
        </div>
    )
}

Though, in a scenario you described, where copying something for the sake of updating is prohibitive inefficient, or bad for other reasons, you actually can do something along the lines of you example. Though, no need to use state then, as React has useRef() hook for such stuff, which should be persistent to the component instance, but should not directly mess with render-triggering logic. So, if you want to go that way, I would do:

const Counter = () => {

    const { current: heap } = useRef({ clicks: 0 });
    const [epoch, setEpoch] = useState(0);

    const increment = () => {
            heap.clicks += 1;
            setEpoch(1 + epoch);
    }

    return (
        <div>
            <p>
                Hi, my name is {name}.
                You have clicked {heap.clicks} times.
            </p>
            <p>
                <button style={{
                    backgroundColor: 'gray',
                    border: '1px solid black'
                }} onClick={increment}>Click Me!
                </button>
            </p>
        </div>
    )
}
Sergey Pogodin
  • 406
  • 2
  • 6
  • Thank you a lot for response. In my example I used an object instead of a number just to be able to mutate it. `useRef` seems to be a great tool. For instance, I do need to store for example a `WebSocket` which, I suppose, has an exclusive ownership for resources. Can `useRef` be used to store and mutate a `WebSocket`, for instance? – Sergey Kolesnik Apr 29 '21 at 18:00
  • @SergeyKolesnik The value of a ref can be whatever you want. – Dave Newton Apr 29 '21 at 18:01
  • Yeap, as @DaveNewton says, it can be anything. I really love the pattern I wrote, because then such `heap` object can be used to store around a lot of different persistent variables, without using individaul `useRef`'s for each of them; _e.g._ when there is a need for some direct DOM manipulations, and lots of intermediate variables sohuld be kept around and updated, but should not trigger re-renders of react tree. – Sergey Pogodin Apr 29 '21 at 18:10
1

Because it basically can't "react" without it. React works by monitoring values and seeing if they have changed. If they have, the things that care about that change react to it. Mutating state directly makes it harder to see that change, partly through convention, but also through how JavaScript does comparisons.

If you change a property directly on an object in JavaScript, it's still the same object. React doesn't do deep comparisons, they are expensive and would depend on the number of properties the objects have. It's much faster to just check if they are the same object.

const a = {foo: "bar"};
const b = a;

console.log(a === b); // true of course

a.foo = "fizz";

console.log(a === b); // still true

What react wants you to do is create an entirely new object whenever you change a property and treat state as immutable.

let a = {foo: "bar"};
let b = a;

console.log(a === b); // true

a = {...a, foo: "fizz"};

console.log(a === b); // false

In the second example, things that care about the change to state would trigger because the object has changed. Additionally, I would suggest not worrying about the speed of copying to new objects until you have an MVP and are able to properly profile your application. You will likely find that the cost is negligible in the overall scheme of things. See Does spread operator affect performance?.


Could you get away with directly mutating state? Sure, probably. Just know that it will make things suddenly harder to debug and the next developer who is expecting the immutable convention will scratch their head and wonder why this wasn't caught by the React linting checks.

zero298
  • 25,467
  • 10
  • 75
  • 100
  • Well, I did mention that you have to write an understandable code. Everything you said is valid. But in my example I do create an entirely new object in `setUpdateRequired({})`. And it did force the component to rerender. I also can't understand what to do with objects with exclusive resource ownership like `WebSocket` or `RTCPeerConnection`. I consider global variables a greater evil than direct mutation. – Sergey Kolesnik Apr 29 '21 at 17:55
  • 1
    @SergeyKolesnik For those things, you would likely combine a [Context](https://reactjs.org/docs/context.html) and a `useRef` such that the context holds the only instance and that instance is long lived. You would have additional backing state within the context that would be updated in reaction to events that take place or come from the socket. `useRef` is sort of your escape hatch to purposefully break reactivity. – zero298 Apr 29 '21 at 17:59
  • that is what @SergeyPogodin suggested in another answer. Is it though possible to avoid storing an object globally? – Sergey Kolesnik Apr 29 '21 at 18:03
  • Do you consider context global? You can also have a socket connection per component, but you'll likely end up destroying and recreating the connection more than you want. Even then, you would probably need a `useRef` for those things to persist between re-renders. – zero298 Apr 29 '21 at 18:06
  • @SergeyKolesnik again, you can store persitstent stuff where you need it: you need it within a single component only, for its lifetime - use `useRef`; if you need it (semi-)globally go with Context API. React is very flexible in that respect, if you read in-between the lines of documentation, and have a deeper understanding what different hooks / APIs do under the hood. – Sergey Pogodin Apr 29 '21 at 18:14
  • It seems to me that context is global by traits. I have a WebRTC video component, so it is a socket per component. I didn't say I don't want to use `useRef`, I don't want to use Context – Sergey Kolesnik Apr 29 '21 at 18:14
  • Well, Context is (semi-)global: it is "global" within children tree. So, if you need to share a socket with a group of components, Context is great for you: just provide a separate context instance per a group of components :) – Sergey Pogodin Apr 29 '21 at 18:17