Ref interface:
// create
const count = useRef(0);
// update (does not trigger render)
count.current = 5;
// access
console.log(count.current);
State variable interface:
//create
const [count, setCount] = useState(0);
// update (triggers render)
setCount(5);
// access
console.log(count);
Subjective Context: the background behind the question
I find it a bit odd that two Hooks that are both fundamentally about doing the same thing — storing and updating data that persists through renders — have such different interfaces for accessing and updating their data. Updates to state variables trigger a render and updates to refs don't, but this difference alone doesn't feel like it would necessitate differences in their respective interfaces for accessing and updating. And I suppose if I'm completely transparent, I think state variables feel less cumbersome to use because I dislike needing to type .current to access the value for a ref, and I find myself wishing refs had the interface that state variables have.
That said, I'm guessing that these differences aren't arbitrary, and that there are practical reasons for why these interfaces differ. Presumably there's something that I don't understand about either JavaScript or React, and if I knew this something then it would make sense to me why refs and state variables were implemented with differences in how they are accessed and updated.
The Actual Objective Question:
Would it have been possible for refs to have been implemented so that they had the same interface as useState, with the only difference between the two being that one triggered renders and one didn't? That interface would be:
Ref, but with the same interface as state variables:
//create
const [count, setCount] = useRef(0);
// write (does not trigger render)
setCount(5);
// read
console.log(count);
If not, why?
The goal of a ref is to have a value which persists between renders, can be changed, but does not rerender.
This can't be implemented by returning a value which you overwrite locally, such as the following:
let val = useRef(0);
val = 2; // Changing it to 2
That will change it in your local code, but react has no idea you did this, so it can't give you the new value on the next render.
Slightly better would be having some function you could call to let react know that you want to change it:
const [val, setVal] = useRef(0);
setVal(2)
Then at least react would know you want to change it. But now your local code doesn't know about the change. The component is not going to rerender any time soon, so you can not get a new val; you're stuck with whatever you had when the render happened. If, for example, you call setVal(2); console.log(val), the console.log will still log out the old value, because nothing has change your local variable.
The only way to meet all the goals of useRef is with a mutable object:
const val = useRef(0);
val.current = 2;
Your code can see the modified value, because you have access to val. React can see the modified value because it too has access to val, and it will return val to you on the next render.
The choice of the name "current" is more or less arbitrary, but the use of a mutable object is unavoidable given the requirements.
As for state, since there's always a rerender, the design i showed above with setVal works just fine. React will simply return the new value to us during the render.
Would it have been possible for refs to have been implemented so that they had the same interface as useState, with the only difference between the two being that one triggered renders and one didn't? That interface would be:
const [count, setCount] = useRef(0); setCount(5); console.log(count);
You seem to be missing that this just declared a const count = 0. No code, with no magic whatsoever, can change that constant variable to a different value. And even if you had used let, this interface would not work, since useRef() and setCount don't know anything about the variable that was created with the destructuring pattern. They could not update it.
Both [hooks] fundamentally about doing the same thing — storing and updating data that persists through renders
Not really, no. useRef gives you a stable reference to the same (mutable) object during every render, but it doesn't store and update any data for that; your code does the mutation itself. And useState doesn't really update any data either; it gives you a way to access the state during rendering, and a tool to rerender the component with a new state value.
Notice that useRef(…) is more or less just an alias for useState({current: …})[0] (apart from optimisations like not creating the tuple and the new init object on every render, and not creating the setter function) - it creates a state that is initialised with an object when the component mounts, and which never changes (to a different object). By offering a separate hook, React mainly allows expressing the intention that you are going to mutate that object, which is otherwise (in general) an antipattern.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With