Use of ref to prevent often-changing value from useCallback - reactjs

In the Reach Hooks FAQ section "How to read an often-changing value from useCallback?" there is an example:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // Write it to the ref
});
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // Read it from the ref
alert(currentText);
}, [textRef]); // Don't recreate handleSubmit like [text] would do
return (
<>
<input value={text} onChange={e => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} />
</>
);
}
I was so confused:
Instead of useCallback updating the function each time the text changes, this code moves it to textRef in useEffect. But isn't that the same because the callback function doesn't change but you still need 1 step to update the value in useEffect?
Why is textRef in the dependency array? Isn't textRef a reference which doesn't change between renders? We always receive the same reference so wouldn't it be the same as inputting an empty array?

instead of useCallback update the function each time the text change, this code move it to textRef in useEffect but isn't that the same because the callback function doesn't change but you still need 1 step to update the value in useEffect?
With the useRef setup, each render re-runs useEffect but handleSubmit stays the same. In a normal setup, each render would re-run useCallback and handleSubmit would be a new reference to a new function. I can see why on the surface this seems like the same amount of work.
The benefits of the ref approach are what other re-renders happen based on a re-rendering of Form. Each time the value of text changes, the parent Form re-renders and the child input re-renders since it takes text as a prop. ExpensiveTree does not re-render because its prop handleSubmit maintains the same reference. Of course the name ExpensiveTree is designed to tell us that we want to minimize re-rendering of that component since it is computationally expensive.

Related

Prevent re-render while using custom hooks

I'm experiencing something similar to this:
Should Custom React Hooks Cause Re-Renders of Dependent Components?
So, I have something like this:
const ComponentA = props => {
const returnedValue = useMyHook();
}
And I know for a fact that returnedValue is not changing (prev. returnedValue is === to the re-rendered returnedValue), but the logic inside of the useMyHook does cause internal re-renders and as a result, I get a re-render in the ComponentA as well.
I do realize that this is intentional behavior, but what are my best options here? I have full control over useMyHook and returnedValue. I tried everything as I see it, caching(with useMemo and useCallback) inside of useMyHook on returned value etc.
Update
Just to be more clear about what I'm trying to achieve:
I want to use internal useState / useEffect etc inside of useMyHook and not cause re-render in the ComponentA
Try to find out the reason of re-rendering in your hook, probably caused due to a state update. If it is due to updation of states in useEffect then give the states and values as dependency. If your states are updating in a function, do try to call the function in order to invoke.
If these doesn't work, try to use ref. useRef hook can be used to prevent such re-renders as it will provide you with .current values.
It would be better if you remove your useEffect dependencies one by one so that you can know what dependency is causing the error and create a ref for the same.
You can share a codesandbox and I would be happy to help you out with it.
I want to use internal useState / useEffect etc inside of useMyHook and not cause re-render in the ComponentA
If you want to avoid the renders triggered by useState() or useEffect() then they are not the right tools.
If you need to hold some mutable state without triggering renders then consider using useRef() instead.
This question has a nice comparison of the differences between useState() and useRef().
Here is a simple example:
const ComponentState = () => {
const [value, setValue] = useState(0);
return (<>
<button onClick={() => {
// Will trigger render
setValue(Math.random());
}></button>
</>);
}
const ComponentRef = () => {
const valueRef = useRef(0);
return (<>
<button onClick={() => {
// Will not trigger render
valueRef.current = Math.random();
}></button>
</>);
}

React: why is that changing the current value of ref from useRef doesn't trigger the useEffect here

I have a question about useRef: if I added ref.current into the dependency list of useEffect, and when I changed the value of ref.current, the callback inside of useEffect won't get triggered.
for example:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Also I know I can use useState here. This is not what I am asking. And also I know that ref stay referentially the same during re-renders so it doesn't change. But I am not doing something like
const myRef = useRef(1);
useEffect(() => {
//...
}, [myRef]);
I am putting the current value in the dep list so that should be changing.
I know I am a little late, but since you don't seem to have accepted any of the other answers I'd figure I'd give it a shot too, maybe this is the one that helps you.
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Short answer, no.
The only things that cause a re-render in React are the following:
A state change within the component (via the useState or useReducer hooks)
A prop change
A parent render (due to 1. 2. or 3.) if the component is not memoized or otherwise referentially the same (see this question and answer for more info on this rabbit hole)
Let's see what happens in the code example you shared:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Initial render
myRef gets set to {current: 1}
The effect callback function gets registered
React elements get rendered
React flushes to the DOM (this is the part where you see the result on the screen)
The effect callback function gets executed, "myRef current changed" gets printed in the console
And that's it. None of the above 3 conditions is satisfied, so no more rerenders.
But what happens when you click the button? You run an effect. This effect changes the current value of the ref object, but does not trigger a change that would cause a rerender (any of either 1. 2. or 3.). You can think of refs as part of an "effect". They do not abide by the lifecycle of React components and they do not affect it either.
If the component was to rerender now (say, due to its parent rerendering), the following would happen:
Normal render
myRef gets set to {current: 1} - Set up of refs only happens on initial render, so the line const myRef = useRef(1); has no further effect.
The effect callback function gets registered
React elements get rendered
React flushes to the DOM if necessary
The previous effect's cleanup function gets executed (here there is none)
The effect callback function gets executed, "myRef current changed" gets printed in the console. If you had a console.log(myRef.current) inside the effect callback, you would now see that the printed value would be 2 (or however many times you have pressed the button between the initial render and this render)
All in all, the only way to trigger a re-render due to a ref change (with the ref being either a value or even a ref to a DOM element) is to use a ref callback (as suggested in this answer) and inside that callback store the ref value to a state provided by useState.
https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
use useCallBack instead, here is the explanation from React docs:
We didn’t choose useRef in this example because an object ref doesn’t
notify us about changes to the current ref value. Using a callback ref
ensures that even if a child component displays the measured node
later (e.g. in response to a click), we still get notified about it in
the parent component and can update the measurements.
Note that we pass [] as a dependency array to useCallback. This
ensures that our ref callback doesn’t change between the re-renders,
and so React won’t call it unnecessarily.
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Ok so I think what you're missing here is that changing a ref's value doesn't cause a re-render. So if it doesn't cause re-renders, then the function doesn't get run again. Which means useEffect isn't run again. Which means it never gets a chance to compare the values. If you trigger a re-render with a state change you will see that the effect will now get run. So try something like this:
export default function App() {
const [x, setX] = useState();
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<button
onClick={() => {
myRef.current = myRef.current + 1;
// Update state too, to trigger a re-render
setX(Math.random());
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
);
}
Now you can see it will trigger the effect.

Child re-rendering because of function?

I'm having trouble navigating these concepts where a child keeps re-rendering because I'm passing it a function from the parent. This parent function references an editor's value, draftjs.
function Parent() {
const [doSomethingValue, setDoSomethingValue] = React.useState("");
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty()
);
const editorRef = useRef<HTMLInputElement>(null);
const doSomething = () => {
// get draftjs editor current value and make a fetch call
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
// do something with userResponse
setDoSomethingValue(someValue);
}
return (
<React.Fragment>
<Child doSomething={doSomething} />
<Editor
ref={editorRef}
editorState={editorState}
onChange={setEditorState}
placeholder="Start writing..." />
<AnotherChild doSomethingValue={doSomethingValue}
<React.Fragment>
}
}
My Child component is simply a button that calls the parent's doSomething and thats it.
doSomething does its thing and then makes a change to the state which is then passed to AnotherChild.
My problem is that anytime the editorState is updated (which is every time you type within the editor), my Child component re-renders. Isn't that unnecessary? And if so, how could I avoid this?
If I was passing my Child component a string and leveraged React.Memo, it does not re-render unless the string changes.
So what am I missing with passing a function to the child? Should my child be re-rendering everytime?
React works on reference change detection to re-render components.
Child.js: Wrap it under React.memo so it becomes Pure Component.
const Child = ({doSomething}) => <button onClick={doSomething}>Child Button Name</button>;
export default React.memo(Child);
Parent.js -> doSomething: On every (re)render, callbacks are also recreated. Make use of useCallback so that your function is not recreated on every render.
const doSomething = React.useCallback(() => {
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
setDoSomethingValue(someValue);
}, [editorState]);
Side Note
On broader lines, memo is HOC and makes a component Pure Component. useMemo is something which cache the output of the function. Whereas useCallback caches the instance of the function.
Hope it helps.
If you do not want your component to be re-rendered every time your parent is re-rendered, you should take a look at useMemo.
This function will only recalculate its value (in your case, your component) whenever its second argument changes (here, the only thing it depends on, doSomething()).
function Parent() {
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty()
);
const editorRef = useRef<HTMLInputElement>(null);
const doSomething = () => {
// get draftjs editor current value and make a fetch call
let userResponse = editorState.getCurrentContent().getPlainText("\u0001");
// do something with userResponse
}
const childComp = useMemo(() => <Child doSomething={doSomething} />, [doSomething])
return (
<React.Fragment>
{childComp}
<Editor
ref={editorRef}
editorState={editorState}
onChange={setEditorState}
placeholder="Start writing..." />
<React.Fragment>
}
}
If doSomething does not change, your component does not re-render.
You may also want to use useCallback for your function if it is doing heavy calculations, to avoid having it re-compiled every time your component renders: https://reactjs.org/docs/hooks-reference.html#usecallback
Take a look into PureComponent, useMemo, or shouldComponentUpdate
I would also add, instead of passing down the function to do the rendering of a top level component, pass the value and define the function later down in the component tree.
if you want to avoid unnecessary re-renders, you can use React.memo and the hook useCallback. Take a look at the following sandbox.
The button1 is always re-rendered because it take a callback that is not memoized with useCallback and the button2 is just rendered the first time even if the state of the parent has changed (take a look at the console to check the re-renders). You have to use React.memo in the Child component that is the responsible to render the buttons.
I hope it helps.

useEffect simulating componentWillUnmount does not return updated state

I have a functional component that initializes a state with useState, then this state is changed via an input field.
I then have a useEffect hook simulating componentWillUnmount so that, before the component unmounts, the current, updated state is logged to the console. However, the initial state is logged instead of the current one.
Here is a simple representation of what I am trying to do (this is not my actual component):
import React, { useEffect, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
useEffect(() => {
return () => {
console.log(text);
}
}, [])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
I initialize the state as "initial." Then I use the input field to change the state, say I type in "new text." However, when the component in unmounted, "initial" is logged to the console instead of "new text."
Why does this happen? How can I access the current updated state on unmount?
Many thanks!
Edit:
Adding text to useEffect dependency array doesn’t solve my problem because in my real-world scenario, what I want to do is to fire an asynchronous action based on the current state, and it wouldn’t be efficient to do so everytime the “text” state changes.
I’m looking for a way to get the current state only right before the component unmounts.
You've effectively memoized the initial state value, so when the component unmounts that value is what the returned function has enclosed in its scope.
Cleaning up an effect
The clean-up function runs before the component is removed from the UI
to prevent memory leaks. Additionally, if a component renders multiple
times (as they typically do), the previous effect is cleaned up before
executing the next effect. In our example, this means a new
subscription is created on every update. To avoid firing an effect on
every update, refer to the next section.
In order to get the latest state when the cleanup function is called then you need to include text in the dependency array so the function is updated.
Effect hook docs
If you pass an empty array ([]), the props and state inside the effect
will always have their initial values. While passing [] as the second
argument is closer to the familiar componentDidMount and
componentWillUnmount mental model, there are usually better solutions
to avoid re-running effects too often.
This means the returned "cleanup" function still only accesses the previous render cycle's state and props.
EDIT
useRef
useRef returns a mutable ref object whose .current property is
initialized to the passed argument (initialValue). The returned object
will persist for the full lifetime of the component.
...
It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.
Using a ref will allow you to cache the current text reference that can be accessed within the cleanup function.
/EDIT
Component
import React, { useEffect, useRef, useState } from 'react';
const Input = () => {
const [text, setText] = useState('aaa');
// #1 ref to cache current text value
const textRef = useRef(null);
// #2 cache current text value
textRef.current = text;
useEffect(() => {
console.log("Mounted", text);
// #3 access ref to get current text value in cleanup
return () => console.log("Unmounted", text, "textRef", textRef.current);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
console.log("current text", text);
return () => {
console.log("previous text", text);
}
}, [text])
const onChange = (e) => {
setText(e.target.value);
};
return (
<div>
<input type="text" value={text} onChange={onChange} />
</div>
)
}
export default Input;
With the console.log in the returned cleanup function you'll notice upon each change in the input the previous state is logged to console.
In this demo I've logged current state in the effect and previous state in the cleanup function. Note that the cleanup function logs first before the current log of the next render cycle.

Is it possible to get rid of useContext calculations each rerender?

We can prevent unnecessary calculations for useEffect with an empty array as the second argument in the hook:
// that will be calculated every single re-rendering
useEffect(() => {...some procedures...})
// that will be calculated only after the first render
useEffect(() => {...some procedures...}, [])
But, for the useContext hook we can't do like above, provide the second argument.
Also, we can't wrap useContext by useCallback, useMemo.
For instance, we have a component:
const someComponent = () => {
const contextValue = useContext(SomeContext);
const [inputValue, setInputValue] = useState('');
return (
<Fragment>
<input value={inputValue} onChange={onInputChange} />
<span>{contextValue}</span>
</Fragment>
)
The problem is that every single typing will launch re-rendering and we will have unnecessary useContext re-rendering each time. One of the decision is brake the component on two:
const WithContextDataComponent = () => {
const contextValue = useContext(SomeContext);
return <JustInputComponent contextValue={contextValue} />
const JustInputComponent = (props) => {
const [inputValue, setInputValue] = useState('');
return <input value={inputValue} onChange={onInputChange} />
So, now the problem is disappeared, but two components we have, though. And in the upper component instead <SomeComponent /> we should import <WithContextDataComponent />, that a little bit ugly, I think.
Can I stop the unnecessary re-rendering for useContext without splitting into two components?
From React Hooks API Reference:
https://reactjs.org/docs/hooks-reference.html#usecontext
useContext
const value = useContext(MyContext);
Accepts a context object (the value returned from React.createContext) and returns the current context value for that context. The current context value is determined by the value prop of the nearest above the calling component in the tree.
When the nearest above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider.
As you can see from the documentation, the useContext() hook will only cause a re-render of your component if the values that it provides change at some point. Ans this is probably your intended behavior. Why would you need stale data in your context hook?
When your component re-render by itself, without changes in the context, the useContext() line will simply give back the same values that it did on the previous render.
It seems that you're using the useContext() hook the way it it meant to be used. I don't see anything wrong with it.

Resources