React: Difference between side effect in component's function vs effect? - javascript

I'm trying to understand the practical difference between having a side effect in a component function, vs having it within an effect which has no dependency array passed in (and as such should fire on every render). From what I can observe, they both run at the same frequency. I realize that effects allow for cleanup at the proper time, but I'm simply curious about the scenario where cleanup is not a factor.
The following CodePen shows what I'm talking about.
https://codepen.io/benrhere/pen/GRyvXZZ
The important piece is:
function EffectVsFunctionQuestion() {
const [count, setCount] = React.useState(0);
React.useEffect(()=>{
console.log("incremented within hook")
});
console.log("incremented within component function")
...
}

Side-effects issued from the useEffect hook have the benefit of being triggered at most once per render cycle. The render cycle here means the "commit phase" when React has computed the next view and commits it to the DOM.
This should not be confused with what React calls the "render phase" when it renders out the component (and children and entire ReactTree) to compute what changed and needs to be committed to the DOM.
The entire function body of a React function component is the "render" function. As you can see in the diagram, any unintentional side-effects in the render or body will occur during the "render phase" which can can be paused, aborted, or restarted (i.e. run again) by React. Note that the "render phase" is to be pure and free from side-effects.
The "commit phase" the component can work with the DOM and run side-effects.
Why does this matter? How to tell the difference.?
React actually ships with a StrictMode component that helps you detect unexpected side-effects.
Detecting unexpected side effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
Here's an example sandbox demo that demonstrates the unexpected side effects.
Note that the unexpected effect is doubled.
Code:
const externalValue1 = { count: 0 };
const externalValue2 = { count: 0 };
function EffectVsFunctionQuestion() {
const [state, setState] = React.useState(0);
React.useEffect(() => {
externalValue1.count++;
console.log("incremented within hook", externalValue1.count);
});
externalValue2.count++;
console.log("incremented within component function", externalValue2.count);
return (
<button type="button" onClick={() => setState((c) => c + 1)}>
Render
</button>
);
}

Essentially the useEffect hook ensures that code will not run until the DOM is mounted/updated. If you run code outside of it, it can be called before the DOM renders, which could lead to issues. It won't block content from rendering, since it's called after render. It's the equivalent of several of the previous class based lifecycle methods (ComponentDidMount, etc.). Docs

Related

Is it okey to use side effects in the useState hook callback?

Imagine situation:
const [value, setValue] = useState(false);
const setSomething = (val) => {
setValue((prev) => {
fn(); dispatch(action); // or any other side effect
return prev + val;
});
};
Is it programmatically okey and fine with react principles to call side effects inside useState callback? May it affect the render process somehow?
It is not ok to use side effects inside the updater function. It might affect the render process, depending on the specific side effect.
It is not fine with react principles (separation of concerns, declarative code).
(I remember to have seen some exceptional use cases where putting some code inside the updater function was said to be the only solution, but I can't remember what it was. I'd appreciate an example in the comments.)
1. Consequences of using side effects
It is not ok to use side effects, basically for the same reasons why you shouldn't use side effects outside useEffect anywhere else.
Some side effects might affect the render process, other side effects might work fine (technically), but you are not supposed to rely on what happens inside the setter functions.
React guarantees that e.g. if you call setState( prev => prev + 1 ), then state would now be one more than before.
React does not guarantee what will happen behind the scenes to achieve that goal. React might call these setter functions multiple times, or not at all, and in any order:
StrictMode - Detecting unexpected side effects
... Because the above methods might be called more than once, it’s important that they do not contain side-effects. ...
2. following react principles
You should not put side effects inside the updater function, because it validates some principles, like separation of concerns and writing declarative code.
Separation of concerns:
setCount should do nothing but setting the count.
Writing declarative code:
Generally, you should write your code declarative, not imperative.
I.e. your code should "describe" what the state should be, instead of calling functions one after another.
I.e. you should write "B should be of value X, dependent on A" instead of "Change A, then change B"
In some cases React doesn't "know" anything about your side effects, so you need to take care about a consistent state yourself.
Sometimes you can not avoid writing some imperative code.
useEffect is there to help you with keeping the state consistent, by allowing you to e.g. relate some imperative code to some state, aka. "specifying dependencies".
If you don't use useEffect, you can still write working code, but you are just not using the tools react is providing for this purpose. You are not using React the way it is supposed to be used, and your code becomes less reliable.
Examples for problems with side effects
E.g. in this code you would expect that A and B are always identical, but it might give you unexpected results, like B being increased by 2 instead of 1 (e.g. when in DEV mode and strict mode):
export function DoSideEffect(){
const [ A, setA ] = useState(0);
const [ B, setB ] = useState(0);
return <div>
<button onClick={ () => {
setA( prevA => { // <-- setA might be called multiple times, with the same value for prevA
setB( prevB => prevB + 1 ); // <-- setB might be called multiple times, with a _different_ value for prevB
return prevA + 1;
} );
} }>set count</button>
{ A } / { B }
</div>;
}
E.g. this would not display the current value after the side effect, until the component is re-rendered for some other reason, like increasing the count:
export function DoSideEffect(){
const someValueRef = useRef(0);
const [ count, setCount ] = useState(0);
return <div>
<button onClick={ () => {
setCount( prevCount => {
someValueRef.current = someValueRef.current + 1; // <-- some side effect
return prevCount; // <-- value doesn't change, so react doesn't re-render
} );
} }>do side effect</button>
<button onClick={ () => {
setCount(prevCount => prevCount + 1 );
} }>set count</button>
<span>{ count } / {
someValueRef.current // <-- react doesn't necessarily display the current value
}</span>
</div>;
}
No, it is not ok to issue side-effects from a state updater function, it is to be considered a pure function.
The function return values are identical for identical arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams), and
The function application has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or input/output streams).
You may, or may not, be using the React.StrictMode component, but it's a method to help detect unexpected side effects.
Detecting unexpected side effects
Conceptually, React does work in two phases:
The render phase determines what changes need to be made to
e.g. the DOM. During this phase, React calls render and then
compares the result to the previous render.
The commit phase is
when React applies any changes. (In the case of React DOM, this is
when React inserts, updates, and removes DOM nodes.) React also calls
lifecycles like componentDidMount and componentDidUpdate during
this phase.
The commit phase is usually very fast, but rendering can be slow. For
this reason, the upcoming concurrent mode (which is not enabled by
default yet) breaks the rendering work into pieces, pausing and
resuming the work to avoid blocking the browser. This means that React
may invoke render phase lifecycles more than once before committing,
or it may invoke them without committing at all (because of an error
or a higher priority interruption).
Render phase lifecycles include the following class component methods:
constructor
componentWillMount (or UNSAFE_componentWillMount)
componentWillReceiveProps (or UNSAFE_componentWillReceiveProps)
componentWillUpdate (or UNSAFE_componentWillUpdate)
getDerivedStateFromProps
shouldComponentUpdate
render
setState updater functions (the first argument) <--
Because the above methods might be called more than once, it’s
important that they do not contain side-effects. Ignoring this rule
can lead to a variety of problems, including memory leaks and invalid
application state. Unfortunately, it can be difficult to detect these
problems as they can often be non-deterministic.
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState) <--
Functions passed to useState, useMemo, or useReducer
Take a cue from the two highlighted bullet points regarding the intentional double-invoking of state updater functions and treat the state updater functions as pure functions.
For the code snippet you shared, I see no reason at all for the functions to be called from within the updater callback. They could/should be called outside the callback.
Example:
const setSomething = (val) => {
setValue((prev) => {
return prev + val;
});
fn();
dispatch(action);
};
I would not
Just because it works doesn't mean it's a good idea. The code sample you shared will function, but I wouldn't do it.
Putting unrelated logic together will confuse the next person who has to work with this code; very often, that "next person" is you: you, six months from now, after you've forgotten all about this code because you finished this feature and moved on. And now you come back and discover that some of the silverware has been stored in the bathroom medicine cabinet, and some of the linens are in the dishwasher, and all the plates are in a box labeled "DVDs".
I don't know how serious you are about the specific code sample you posted, but in case it's relevant: if you're using dispatch that means you've set up some kind of reducer, either with the useReducer hook, or possibly with Redux. If that's true, you should probably consider whether this boolean belongs in your Redux store, too:
const [ value, setValue ] = useState(false)
function setSomething(val) {
fn()
dispatch({ ...action, val })
}
(But it might not, and that's fine!)
If you're using actual Redux, you'll also have action-creators, and that's generally the correct place to put code that triggers side effects.
Regardless of whatever state tech you're using, I think you should prefer to avoid putting side-effect code into your individual components. The reason is that components are generally supposed to be reusable, but if you put a side-effect into the component that is not essential to display or interaction of the thing being visualized by the component, then you've just made it harder for other callers to use this component.
If the side-effect is essential to how this component works, then a better way to handle this would be to call setValue and the side-effect function directly instead of wrapping them up together. After all, you don't actually depend on the useState callback to accomplish your side-effect.
const [ value, setValue ] = useState(false)
function setSomething(val) {
setValue(value + val)
fn()
dispatch(action)
}

useCallback: When to use it? Is there any drawback / performance issue?

There are articles related to what is useCallback and to use it when necessary. But in a big project it's very difficult to understand when to start using this.
So, if I use it for every use case, will it hit the performance of the application.
useCallback and useMemo are helper hooks with the main purpose off adding an extra layer of dependency check to ensure synchronicity. Usually you want to work with useCallback to ensure a stable signature to a prop which you know how will change and React doesn't.
A function(reference type) passed via props for example
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [])
}
Lets assume you have a child component which uppon mounting must set some state in the parent (not usual), the above code will generate a warning of undeclared dependency in useEffect, so let's declare setParentState as a dependency to be checked by React
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [setParentState])
}
Now this effect runs on each render, not only on mounting, but on each update. This happens because setParentState is a function which is recreated every time the function Component gets called. You know that setParentState won't change it's signature overtime so it's safe to tell React that. By wrapping the original helper inside an useCallback you're doing exactly that (by adding another dependency check layer).
const Component = ({ setParentState }) =>{
const stableSetter = useCallback(() => setParentState(), [])
useEffect(() => setParentState('mounted'), [stableSetter])
}
There you go. Now React knows that stableSetter won't change it's signature inside the lifecycle therefore the effect do not need run unecessarily.
On a side note useCallback it's also used like useMemo, to optmize expensive function calls (memoization).
The two main purposes of useCallback are
Optimize child components that rely on reference equality to prevent unnecessary
renders. Font
Memoize expensive computing calculations
Is there any drawback/performance?
useCallback is mainly used to optmize performance by changing stuff only when it's needed. It does that by adding a layer of dependencies just like useEffect (and similar to how React itself knows how something must change in the DOM), but as every single performance optimization technique, it can shoot backwards, if you fill your entire application with unecessary useCallback, useMemo, React.memo the performance will go the other way around.
So the key is to use wisely, carefully choosing what needs to have a stable signature and what doesn't.

Component rendering multiple times without any reason

I was facing multiple re-renders and thus to find the issue I commented every line of code and at last found the constant definition causing re-render:
const MyComponent = () => {
console.log('render') // logs 4 times
const myRef = useRef(null)
return <h1>hello</h1>
}
const MyComponent = () => {
console.log('render') // logs 2 times
return <h1>hello</h1>
}
I know strict mode renders the component 2 times. But, why just defining myRef constant causing re-render further 2 times?
I just tried React.memo as comment, but it still renders 4 times:
export default React.memo(MyComponent)
Okay, I just tested using the same code in App.js and it only logged 2 times. So, it seems to be parent component issue. But my question is why in the child component without having anything causing renders additional 2 times just defining the variable?
As you and React docs said:
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
Class component constructor, render, and shouldComponentUpdate methods
Class component static getDerivedStateFromProps method
Function component bodies
State updater functions (the first argument to setState)
Functions passed to useState, useMemo, or useReducer
Source: https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects
It does not include useRef but it actually should be on this list because it uses same underlying mechanism as useState
React.memo won't help here because what causes re-render is React.StrictMode special mechanism to check side effects and other stuff, so it ignores memo and similar techniques.
What's interesting here that if you add useRef in Parent component it will invoke function body 2 times, but its children (if they don't use useRef) are gonna be invoked only once.
All this stuff only works for development mode, so in production there won't be any intentional double invocations and other things but they can still happen unintentionally.

Code Execution Directly in a React Function Component, When Multiple States Causes Multiple Re-Renders

I have component that uses both useState() and other custom hooks multiple times.
I want to act based on these state values. I could do that directly in the function component's body:
const MyComponent = () => {
const [someState, setSomeState] = useState(false);
const [otherState, setOtherState] = useState(false);
const customHookValue = useCustomHook();
if (someState) foo();
const foo = () => setOtherState(!otherState);
if (customHookValue > 10) bar();
const bar = () => setSomeState(somestate > customHookValue);
}
However, every time someState changes (and a re-render happens) the second conditional also runs, causing bar() to run if the conditional passes. This feels unnatural. Logically, bar() should only run when customHookValue changes, as someState only changes if customHookValue has changed since last render.
In summary, a re-render caused by a change to someState should not cause a bunch of unrelated state setting functions to run again. Even though re-running them causes no change in the outcome of the program, it is not logically right. They only need to re-run when their corresponding conditional changes. It could effect performance.
This must be a common challenge in React. I am quite new to it, so I do not know what the approach to solve this would be.
Questions
How would I solve the above in the recommended manner?
Would I have to wrap every conditional in a useEffect or a useMemo?
EDIT:
Updated the second conditional, to make my question clearer (it should depend on customHook).
CLARIFICATION:
As it might not have been clear, my issue is as follows. When state changes, a re-render occurs. This causes all functions in the component's body to re-run. Now, if we have many useState in a component, and only one changes, a bunch of potentially unrelated state-changing and potentially expensive functions I have defined in the components body will run. These state-changing functions would only have to run if the state values they are trying to set has changed. If the values they are setting has not changed, it is unnecessary for them to run again. But, the re-render reruns all functions in the component's body regardless.
It looks like (as others have suggested) you want useEffect:
useEffect(() => {
if (someState) {
setOtherState(!otherState)
}
}, [someState, otherState])
useEffect(() => {
if (customHookValue > 10) {
setSomeState(someState > customHookValue)
}
}, [customHookValue])
Since you only want the setSomeState to run if customHookValue changes, make it the only item in the dependencies array passed to useEffect.
The exhaustive-deps eslint-plugin-react-hooks will complain about the second useEffect, since the function depends on the value of someState, even though someState will only potentially change if customHookValue changes. I also wouldn't worry about things potentially affecting performance until they do. I don't know a ton about the internals of React, but it does some things under the hood to avoid re-renders it doesn't need to do, and can do multiple renders before an actual update is painted.

Gatsby: Context update causes infinite render loop

I am trying to update context once a Gatsby page loads.
The way I did it, the context is provided to all pages, and once the page loads the context is updated (done with useEffect to ensure it only happens when the component mounts).
Unfortunately, this causes an infinite render loop (perhaps not in Firefox, but at least in Chrome).
Why does this happen? I mean, the context update means all the components below the provider are re-rendered, but the useEffect should only run once, and thats when the component mounts.
Here is the code: https://codesandbox.io/s/6l3337447n
The infinite loop happens when you go to page two (link at bottom of page one).
What is the solution here, if I want to update the context whenever a page loads?
The correct answer for this issue is not to pass an empty dependency array to useEffect but to wrap your context's mergeData in a useCallback hook. I'm unable to edit your code but you may also need to add dependencies to your useCallback like in my example below
import React, { useState, useCallback } from "react"
const defaultContextValue = {
data: {
// set initial data shape here
menuOpen: false,
},
mergeData: () => {},
}
const Context = React.createContext(defaultContextValue)
const { Provider } = Context
function ContextProviderComponent({ children }) {
const [data, setData] = useState({
...defaultContextValue,
mergeData, // shorthand method name
})
const mergeData = useCallback((newData) {
setData(oldData => ({
...oldData,
data: {
...oldData.data,
...newData,
},
}))
}, [setData])
return <Provider value={data}>{children}</Provider>
}
export { Context as default, ContextProviderComponent }
The selected answer is incorrect because the react docs explicitly say not to omit dependencies that are used within the effect which the current selected answer is suggesting.
If you use es-lint with the eslint-plugin-react-hooks it will tell you this is incorrect.
Note
If you use this optimization, make sure the array includes all values
from the component scope (such as props and state) that change over
time and that are used by the effect. Otherwise, your code will
reference stale values from previous renders. Learn more about how to
deal with functions and what to do when the array changes too often.
https://reactjs.org/docs/hooks-effect.html
Is it safe to omit functions from the list of dependencies? Generally
speaking, no. It’s difficult to remember which props or state are used
by functions outside of the effect. This is why usually you’ll want to
declare functions needed by an effect inside of it. Then it’s easy to
see what values from the component scope that effect depends on:
https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies
By default, useEffect runs every render. In your example, useEffect updates the context every render, thus trigger an infinite loop.
There's this bit in the React doc:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
So applies to your example:
useEffect(() => {
console.log("CONTEXT DATA WHEN PAGE 2 LOADS:", data)
mergeData({
location,
})
- }, [location, mergeData, data])
+ }, [])
This way, useEffect only runs on first mount. I think you can also leave location in there, it will also prevent the infinite loop since useEffect doesn't depend on the value from context.

Categories