I want to set volume on an audio element, after it mounts.
If I have a ref on a DOM node, that isn't conditionally rendered, can I rely on that ref's value being defined once the first useEffect runs?
function Component() {
const ref = React.useRef();
React.useEffect(() => {
// Can ref.current ever be undefined here?
ref.current.volume = 0.4;
}, []);
return <audio src="..." ref={ref} />
}
I've seen a lot of people put if statements and optional chaining for refs inside useEffect. Is that necessary if there's no conditional rendering?
refs are set during commit phase:
React sets ref.current during the commit. Before updating the DOM,
React sets the affected ref.current values to null. After updating the
DOM, React immediately sets them to the corresponding DOM nodes.
And useEffect runs after commit phase:
The function passed to useEffect will run after the render is
committed to the screen. Think of effects as an escape hatch from
React’s purely functional world into the imperative world.
so assuming you actually rendered that element on UI, ref.current should be referring to the DOM node inside effect.
Related
I created ThemeProvider component where it retrieves the design tokens from json which is passed in as tokens prop, then it inserts those tokens into style attribute of parent div. And ThemeProvider wraps the children component so children can use those styles. Since consumers won't be updating tokens often, I just need to run the retrieveToken function once in the initial render.
My question is should I use useEffect with empty dependency or should I be using useMemo to cache the result of retrieveToken function? My guess is useEffect with empty dependency is enough since token prop won't be updated at all, only needed for initial run.
const [tokenObj, setTokenObj] = useState({});
const {tokens} = props;
function retrieveToken(tokens){
// expensive operation, but only needed to run once in initial render
}
//which way is better?
// Option1 - react useMemo
const tokenObj2 = React.useMemo(() => {
return retrieveToken(tokens);
},[tokens]);
//Option2 - react useEffect
React.useEffect(() => {
setTokenObj(retrieveToken(tokens))
},[])
return (
<div style={tokenObj}> {children} </div>
)
useMemo is the right one to use and is purpose made for running expensive synchronous operations less often.
useEffect should not be used for a few reasons:
It is primarily there for handling async logic as a pattern
It is not executed during Server-Side Rendering
It runs after the DOM is rendered (users will briefly see the wrong styles)
Let's say I have this simple dummy component:
const Component = () => {
const [state, setState] = useState(1);
setState(1);
return <div>Component</div>
}
In this code, I update the state to the same value as before directly in the component body. But, this causes too many re-renders even if the value stayed the same.
And as I know, in React.useState, if a state value was updated to the same value as before - React won't re-render the component. So why is it happening here?
However, if I try to do something simillar with useEffect and not directly in the component body:
const Component = () => {
const [state, setState] = useState(1);
useEffect(()=>{
setState(1);
},[state])
return <div>Component</div>
}
This is not causing any infinte loop and goes exactly according to the rule that React won't re-render the component if the state stayed the same.
So my question is: Why is it causing an infinte loop when I do it directly in the component body and in the useEffect it doesn't?
If someone has some "behind the sences" explanation for this, I would be very grateful!
TL;DR
The first example is an unintentional side-effect and will trigger rerenders unconditionally while the second is an intentional side-effect and allows the React component lifecycle to function as expected.
Answer
I think you are conflating the "Render phase" of the component lifecycle when React invokes the component's render method to compute the diff for the next render cycle with what we commonly refer to as the "render cycle" during the "Commit phase" when React has updated the DOM.
See the component lifecycle diagram:
Note that in React function components that the entire function body is the "render" method, the function's return value is what we want flushed, or committed, to the DOM. As we all should know by now, the "render" method of a React component is to be considered a pure function without side-effects. In other words, the rendered result is a pure function of state and props.
In the first example the enqueued state update is an unintentional side-effect that is invoked outside the normal component lifecycle (i.e. mount, update, unmount).
const Component = () => {
const [state, setState] = useState(1);
setState(1); // <-- unintentional side-effect
return <div>Component</div>;
};
It's triggering a rerender during the "Render phase". The React component never got a chance to complete a render cycle so there's nothing to "diff" against or bail out of, thus the render loop occurs.
The other example the enqueued state update is an intentional side-effect. The useEffect hook runs at the end of the render cycle after the next UI change is flushed, or committed, to the DOM.
const Component = () => {
const [state, setState] = useState(1);
useEffect(() => {
setState(1); // <-- intentional side-effect
}, [state]);
return <div>Component</div>;
}
The useEffect hook is roughly the function component equivalent to the class component's componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle methods. It is guaranteed to run at least once when the component mounts regardless of dependencies. The effect will run once and enqueue a state update. React will "see" that the enqueued value is the same as the current state value and won't trigger a rerender.
Similarly you could use the useEffect hook and completely remove the dependency array so it's an effect that would/could fire each and every render cycle.
const Component = () => {
const [state, setState] = useState(1);
useEffect(() => {
setState(1);
});
return <div>Component</div>;
}
Again, the useEffect hook callback is guaranteed to be invoked at least once, enqueueing a state update. React will "see" the enqueued value is the same as the current state value and won't trigger a rerender.
The takeaway here is to not code unintentional and unexpected side-effects into your React components as this results in and/or leads to buggy code.
When invoking setState(1) you also trigger a re-render since that is inherently how hooks work. Here's a great explanation of the underlying mechanics:
How does React.useState triggers re-render?
I am trying to understand the exact difference in terms of how the re-render of function component is caused in one case using plain setState V/s other case which uses functional state update
The relevant code snippet is as below
Case 1 : Causes re-render of the component
const onRemove = useCallback(
tickerToRemove => {
setWatchlist(watchlist.filter(ticker => ticker !== tickerToRemove));
},
[watchlist]
);
Case 2 : Does not cause re-render
const onRemove = useCallback(tickerToRemove => {
setWatchlist(watchlist =>
watchlist.filter(ticker => ticker !== tickerToRemove)
);
}, []);
Full example of both the use-cases can be seen on;
https://codesandbox.io/s/06c-usecallback-final-no-rerenders-bsm64?file=/src/watchlistComponent.js
https://codesandbox.io/s/06b-usecallback-usememo-ngwev?file=/src/watchlistComponent.js:961-970
UPDATE
Full article link
https://medium.com/#guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928#204b
I am a bit confused as to how the re-render of child components is prevented.
In the article it says
"Thankfully, setter function from useState hook supports a functional
variant which comes to our rescue. Instead of calling setWatchlist
with the updated watchlist array, we can instead send a function that
gets the current state as an argument"
However, I am a bit confused whether the re-rendering of child components is prevented because we use empty array (as [] does not changes between renders) V/s prevented because of using setter variant of useState hook ?
Using a functional state update or not is rather irrelevant to the question you are asking about. You appear to be asking why (1) a callback with dependency triggers a rerender versus (2) a callback with empty dependency.
The answer is quite literally very simple. In version (2) you are providing a stable callback reference from the time the component mounts that never changes, whereas in (1) the callback reference changes when the dependency does. Remember that React components rerender when state or props update (a new callback reference is a new prop reference) or when the parent component rerenders. Since the onRemove prop is updating in (1) it triggers a rerender.
How can I prevent a component from re-rendering when its props are not changed?
Code sandbox link/minimal working example. Move your cursor over Canvas and see that the console is logged with many "Canvas re-rendered" messages.
At the top-level, I pass in a const function as a handler to the Canvas. The onMouseMoveHandler updates App's state so that it can update the Details component:
<div className="App">
<Canvas onMouseMoveHandler={onMouseMoveHandler} />
<Details passedProp={getValue} />
</div>
I need it so that Canvas is not re-rendered because the HTML canvas element inside has user-drawn lines. How can I do this?
I would have thought that using React.memo would make it like a pure component so that it is not re-rendered.
I am a very beginner to React and if I am not thinking of the right code structure, let me know how to fix.
In your App.js every time you call setValue, your App component rerenders and creates a new onMouseMoveHandler function.
Your Canvas component uses React.memo but since on each rerender onMouseMoveHandler gets a new reference, it also rerenders.
In order to prevent that you have to wrap your onMouseMoveHandler with useCallback. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. .
Here's the example:
const onMouseMoveHandler = useCallback(() => {
setValue(Math.random());
}, []);
If you wrap onMouseMoveHandler in a useCallback that should solve it. The problem is that when the state changes, your App-component re-renders and the onMouseMoveHandler function gets a new reference. And seeing as it has a new reference, this will cause the Canvas to re-render as well because it sees this as a prop change. Using the useCallback hook you guarantee that the function reference stays intact between renders.
You need to use callBack hook, because every time you change the state of the component App, you redefine the function onMouseMoveHandler so the value change and the props onMouseMoveHandler change so the component Canvas need to re-render.
You can check the modifications here:
https://codesandbox.io/s/determined-water-72uq4?file=/src/App.js:362-380
I am not sure if this is a valid warning by using useEffect around the dependency array, it seems like whenever variable, method or dispatch inside the useEffect are giving warning that
React Hook useEffect has missing dependencies: 'active', 'retrieveUser', and 'dispatch'. Either include them or remove the dependency array of following example, if I just leave it as blank array to perform the componentDidMount functionality
useEffect(() => {
setActive(active);
await retrieveUser(param1, param2);
dispatch(someAction);
}, []). // warning: React Hook useEffect has missing dependencies: 'active', 'retrieveUser', 'param1', 'param2', and 'dispatch'. Either include them or remove the dependency array, but here I just want to perform componentDidMount concept so my dependency list has to be empty
or
useEffect(() => {
await retrieveUser(param1, param2);
dispatch(someAction);
}, [userId]). // warning: React Hook useEffect has missing dependencies: 'retrieveUser', 'param1', 'param2', and 'dispatch'. Either include them or remove the dependency array
Are those valid warning? Especially that I just want to monitor on specific data field, what's the point of adding all inside dispatch or method into the dependency array if I can't add anything into the dependency list(for componentDidMOunt) or I just want to monitor userId, not param1, param2, etc
React is giving this warning because you are using part of your component's state or props in useEffect but have told it not to run the useEffect callback when that state/props changes. React assumes you will want to run that useEffect() whenever the referenced states change so it is giving you a warning. If you were only referencing variables outside of state or props, you would not get this warning. See below for info about conditionally running useEffect.
Since your goal is to only run useEffect only sometimes, you're likely going to need to restructure your component so that different data is in or out of state. It's difficult to recommend more specific solutions without knowing more about the component; but, a good goal is to have the state you utilize in useEffect match up with the changes that you want to trigger a rerender.
There might be other issues with state in your component. For example, I notice that you call setActive(active);. If active is part of the component's state, that means you are setting active to itself. Which isn't necessary. (this is assuming you are following typical naming patterns and likely have a line at the top of your component like:
const [active, setActive] = useState(initialValue);
Conditionally running useEffect
When you provide a second argument to useEffect(), it limits the situations when useEffect runs the callback function on rerenders. This is for performance reasons as explained in the React docs.
In the first useEffect where you provide an empty array, you are telling React to only run those actions (setActive(active)...etc.) when the component mounts and unmounts.
In the second useEffect where you provide an array with userId, you are telling React to only run the callback on mount, unmount, and whenever userId changes.
React is giving you that warning because it knows it won't run useEffect if active...etc or other values change. This can cause different parts of your component to be referencing different states. For example, one part of your component might be using userId = 1 and another part is using userId = 2. This is not how React is designed to work.
Create a hook that executes only once:
const useEffectOnce = effect => {
useEffect(effect, []);
};
use this hook inside your component
useEffectOnce(() => {
const request = async () => {
const result = await retrieveSum(param1, param2);
setResult(result);
};
request();
});
Now, even if your props (param1, param2) change, the effect is called only once. Be very careful when doing this, the component is probably buggy unless your initial props are truly special. I don't see how this is different from disabling the rule with
// eslint-disable-next-line react-hooks/exhaustive-deps
Demo: https://codesandbox.io/s/trusting-satoshi-fngrw?file=/src/App.js