What are the equivalents of the componentDidMount, componentDidUpdate, and componentWillUnmount lifecycle hooks using React hooks like useEffect?
componentDidMount
Pass an empty array as the second argument to useEffect() to run only the callback on mount only.
function ComponentDidMount() {
const [count, setCount] = React.useState(0);
React.useEffect(() => {
console.log('componentDidMount');
}, []);
return (
<div>
<p>componentDidMount: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentDidMount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
componentDidUpdate
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render. useEffect runs on every render including the first. So if you want to have a strict equivalent as componentDidUpdate, you have to use useRef to determine if the component has been mounted once. If you want to be even stricter, use useLayoutEffect(), but it fires synchronously. In most cases, useEffect() should be sufficient.
This answer is inspired by Tholle, all credit goes to him.
function ComponentDidUpdate() {
const [count, setCount] = React.useState(0);
const isFirstUpdate = React.useRef(true);
React.useEffect(() => {
if (isFirstUpdate.current) {
isFirstUpdate.current = false;
return;
}
console.log('componentDidUpdate');
});
return (
<div>
<p>componentDidUpdate: {count} times</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<ComponentDidUpdate />,
document.getElementById("app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
componentWillUnmount
Return a callback in useEffect's callback argument and it will be called before unmounting.
function ComponentWillUnmount() {
function ComponentWillUnmountInner(props) {
React.useEffect(() => {
return () => {
console.log('componentWillUnmount');
};
}, []);
return (
<div>
<p>componentWillUnmount</p>
</div>
);
}
const [count, setCount] = React.useState(0);
return (
<div>
{count % 2 === 0 ? (
<ComponentWillUnmountInner count={count} />
) : (
<p>No component</p>
)}
<button
onClick={() => {
setCount(count + 1);
}}
>
Click Me
</button>
</div>
);
}
ReactDOM.render(
<div>
<ComponentWillUnmount />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react#16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
From React docs:
If you’re familiar with React class lifecycle methods, you can think
of useEffect Hook as componentDidMount, componentDidUpdate, and
componentWillUnmount combined.
By that saying they mean:
componentDidMount is sort of useEffect(callback, [])
componentDidUpdate is sort of useEffect(callback, [dep1, dep2, ...]) - the array of deps tell React: "if one of the deps is change, run the callback after rendering".
componentDidMount + componentDidUpdate is sort of useEffect(callback)
componentWillUnmount is sort of the returned function from the callback:
useEffect(() => {
/* some code */
return () => {
/* some code to run when rerender or unmount */
}
)
With the help of Dan Abramov's phrasing from his blog, and some additions of my own:
While you can use those hooks, it’s not an exact equivalent. Unlike componentDidMount and componentDidUpdate, it will capture props and state. So even inside the callbacks, you’ll see the props and state of the specific render (which means in componentDidMount the initial props and state). If you want to see “latest” something, you can write it to a ref. But there’s usually a simpler way to structure the code so that you don’t have to.
The returned function which supposes to be alternative to componentWillUnmount also is not an exact equivalent, since the function will run every time the component will re-render and when the component will unmount.
Keep in mind that the mental model for effects is different from component lifecycles, and trying to find their exact equivalents may confuse you more than help. To get productive, you need to “think in effects”, and their mental model is closer to implementing synchronization than to responding to lifecycle events.
Example from Dan's blog:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(`You clicked ${count} times`);
}, 3000);
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
If we use the class implementation:
componentDidUpdate() {
setTimeout(() => {
console.log(`You clicked ${this.state.count} times`);
}, 3000);
}
this.state.count always points at the latest count rather than the one belonging to a particular render.
For Simple Explanation, I would like to show a visual reference
As we can see simply in the above picture that for -
componentDidMount :
useEffect(() => {
console.log('componentWillMount');
}, []);
componentDidUpdate :
useEffect(() => {
console.log('componentWillUpdate- runs on every update');
});
useEffect(() => {
console.log('componentWillUpdate - runs if dependency value changes ');
},[Dependencies]);
componentwillUnmount :
useEffect(() => {
return () => {
console.log('componentWillUnmount');
};
}, []);
Here is a good summary from the React Hooks FAQ listing Hooks equivalents for class lifecycle methods:
constructor: Function components don’t need a constructor. You can initialize the state in the useState call. If computing the initial state is expensive, you can pass a function to useState.
getDerivedStateFromProps: Schedule an update while rendering instead.
shouldComponentUpdate: See React.memo below.
render: This is the function component body itself.
componentDidMount, componentDidUpdate, componentWillUnmount: The useEffect Hook can express all combinations of these (including less common cases).
componentDidCatch and getDerivedStateFromError: There are no Hook equivalents for these methods yet, but they will be added soon.
componentDidMount
useEffect(() => { /*effect code*/ }, []);
[] will make the effect only run once at mount time. Usually you better specify your dependencies. To have the same layout timings as componentDidMount, have a look at useLayoutEffect (not needed in most cases).
componentWillUnmount
useEffect(() => { /*effect code*/ ; return ()=> { /*cleanup code*/ } }, [deps]);
componentWillUnmount corresponds to an effect with cleanup.
componentDidUpdate
const mounted = useRef();
useEffect(() => {
if (!mounted.current) mounted.current = true;
else {
// ... on componentDidUpdate
}
});
To have the same layout timings as componentDidUpdate, have a look at useLayoutEffect (not needed in most cases). See also this post for a more detailed look at componentDidUpdate hook equivalents.
Related
enter image description here
Please look at the time printed on the console.
setInterval() looks like being called twice in on interval.
I attached my code below. I'd really appreciate it if you could give me a clue to the solution.
import React, {useState} from 'react';
import AppRouter from './Router';
import {authService} from '../fbase';
function App() {
console.log('App()');
const [isLoggedIn, setIsLoggedIn] = useState(authService.currentUser);
console.log(authService.currentUser);
setInterval(() => {
console.log(Date().toLocaleString());
console.log('In App.js > App()');
console.log(authService.currentUser);
}, 5000);
return <AppRouter isLoggedIn={isLoggedIn}></AppRouter>;
}
export default App;
Every time a state/prop changes your component will re-render. This means that everything that is not wrapped in a hook of some kind will also re-run. I'm not sure why this happens to you, maybe authService.currentUser returns two different values, an initial (probably empty) one when mounting the component and the correct one after it does some calculations (maybe you request the user from the back-end and it takes a while to get back the response -- some more code would be helpful here).
When the actual (correct) value will be returned from the authService to your function component it will re-render thus running setInterval again.
In order to make sure this never happens it's best to wrap our functionalities in a hook. If you want to run the setInterval just ONCE (when mounting the component) we can wrap it in an useEffect with an empty dependency array:
useEffect(() => {
setInterval(() => {
...
}, 5000)
), []}
This means the hook will only run when your component first mounts.
If you need to run it based on some prop change (like when isLoggedIn changes from false to true or viceversa) you can add that to the dependency array and your interval will run every time isLoggedIn state changes:
useEffect(() => {
setInterval(() => {
...
}, 5000)
}, [ isLoggedIn ])
If you only need to run that when isLoggedIn changes from false to true you can also add an if condition in your useEffect like this:
useEffect(() => {
if (isLoggedIn) {
setInterval(() => {
...
}, 5000)
}
}, [ isLoggedIn ])
More than that, as Jose Antonio Castro Castro mentioned, in all of the above cases you need to use a cleanup function in order to stop the interval from running indefinitely when your component unmounts (because it has no way of knowing to stop by itself). This is achieved by returning a function from your effect:
useEffect(() => {
const interval = setInterval(() => {
...
}, 5000)
return () => clearInterval(interval);
), []}
Don't forget that every time your component feels like re-rendering, all of your constants and functions that are called directly at the root of your component will be re-declared / re-run again.
The useEffect, useCallback, useMemo hooks help us with exactly that, but in different ways. Choose and use them wisely!
As in the comments, you need to call setInterval when you mount the component, but you need to stop the timer when it is unmounted.
const App = (props) => {
const [count, setCount] = React.useState(0)
React.useEffect(function appRunTimer() {
// Creates a new timer when mount the component.
const timer = setInterval(() => {
console.log(Date().toLocaleString())
console.log('In App.js > App()')
}, 5000)
// Stops the old timer when umount the component.
return function stopTimer() {
clearInterval(timer)
}
}, [])
console.log('DRAW')
return (
<button onClick={() => {setCount(count + 1)}}>
Click to update component's state: You click me {count} time(s)
</button>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I have subscribed to some updates from a messaging service in useEffect hook. Going through react hooks documentation and resources, I couldn't find answers to some observations with my code. Here is my code snippet:
import React, { useState, useEffect } from "react";
import { messageService } from "../_services";
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const subscription = messageService.onMessage().subscribe(message => {
setCount(count + 1);
});
return () => {
console.log("unmount");
subscription.unsubscribe();
};
}, [count]);
function sendMessage() {
messageService.sendMessage("message");
}
return (
<div className="jumbotron">
<button onClick={sendMessage} className="btn btn-primary mr-2">
Send Message
</button>
{count}
</div>
);
}
export { App };
Now I am unable to understand two behaviors:
Since useEffect hook has count as a dependency, I should get an infinite loop issue but I observed is that unmount is invoked. Does React unmount the App component in anticipation of an infinite loop? I couldn't find this mentioned anywhere if it is doing so
even though unmount is invoked, the value of count is retained. How is it possible
I know one way to solve it is that I need to use useRef to retain the count and remove it from hook's dependencies along with force update whenever there is count update.
What I am looking for is an answer to the two questions above and if there is a better way of doing this
Thanks in advance.
Stackblitz link for working app in case you want to try out
The cleanup effect not only clean-up before the component unmounts but also if a component renders multiple times (as they typically do), the previous effect is cleaned up before executing the next effect.
Also, in your case, count shouldn't be in the useEffect as you can use functional updates.
useEffect(() => {
const subscription = messageService.onMessage().subscribe((message) => {
setCount((p) => p + 1);
});
return () => {
subscription.unsubscribe();
};
}, []);
Even though the unmount is invoked, the value of count is retained. How is it possible?
Thats due to closures.
I`m having some problems trying to listen to state changes in this application. Basically I was expecting a useEffect hook to be fired after some state changed, but nothing at all is happening.
This is what I got
index.jsx
// this is a simplification.
// I actually have a react-router-dom's Router wrapping everything
// and App is a Switch with multiple Route components
ReactDOM.render(
<Provider>
<App>
</Provider>
, document.getElementById('root'));
useSession.jsx
export const useSession = () => {
const [session, setSession] = useState(null)
const login = useCallback(() => {
// do something
setSession(newSession)
})
return {
session,
setSession,
login
}
}
Provider.jsx
const { session } = useSession();
useEffect(() => {
console.log('Print this')
}, [session])
// more code ...
App.jsx
export function Login() {
const { login } = useSession();
return <button type="button" onClick={() => { login() }}>Login</button>
}
Well I have this Parent component Provider watching the session state, but when it is updated the useEffect of the provider is never called.
The useEffect is fired only if the setSession is called in the same hook/method. For example, if I import the setSession in the Provider and use it there, the useEffect will be fired; Or if I add a useEffect in the useSession method, it is gonna be fired when login updates the state.
The callback of useEffect is called but only once, when the component is mounted, but not when the state is changed.
How can I achieve this behavior? Having the Provider's useEffect fired whenever session is updated?
Thanks in advance!
I think this is just a bit of misunderstanding of how custom hooks work.Every instance of the component has its own state. Let me just show a simple example illustrating this.
function App () {
return (
<div>
<ComponentA/>
<ComponentB/>
<ComponentC/>
<ComponentD/>
</div>
)
}
function useCounter() {
const [counter, setCounter] = React.useState(0);
function increment() {
setCounter(counter+1)
}
return {
increment, counter, setCounter
}
}
function ComponentA() {
const { counter, increment }= useCounter()
return (
<div>
<button onClick={()=>increment()}>Button A</button>
ComponentA Counter: {counter}
</div>
)
}
function ComponentB() {
const { counter, increment }= useCounter()
return (
<div>
<button onClick={()=>increment()}>Button B</button>
ComponentB Counter: {counter}
</div>
)
}
function ComponentC() {
const { counter }= useCounter();
return (
<div>
ComponentC Counter: {counter}
</div>
)
}
function ComponentD() {
const [toggle, setToggle] = React.UseState(false);
const { counter }= useCounter();
React.useEffect(() => {
setInterval(()=>{
setToggle(prev => !prev);
}, 1000)
})
return (
<div>
ComponentD Counter: {counter}
</div>
)
}
From the above code if you can see that incrementing count by clicking Button Awill not affect the count instance of ComponentB.This is because every instance of the component has its own state. You can also see that clicking either buttons won't trigger ComponentC to rerender since they don't share the same instance. Even if i trigger rerender every one second like in Component D thus invoking useCounter the counter in ComponentD remains 0.
Solution
However there are multiple ways of making components share/listen to same state changes
You can shift all your state i.e [session state] to the Provider component and make it visible to other components by passing it via props.
You can move state to a global container Redux or simply use Context Api + UseReducer Hook here is an example
But since you are dealing with auth and session management, I suggest you persist the session state in local storage or session storage, and retrieve it whenever you need it. Hope that helped
This may seem like a weird question, but I do not really see many use cases for useEffect in React (I am currently working on a several thousand-lines React codebase, and never used it once), and I think that there may be something I do not fully grasp.
If you are writing a functional component, what difference does it make to put your "effect" code in a useEffect hook vs. simply executing it in the body of the functional component (which is also executed on every render) ?
A typical use case would be fetching data when mounting a component : I see two approaches to this, one with useEffect and one without :
// without useEffect
const MyComponent = () => {
[data, setData] = useState();
if (!data) fetchDataFromAPI().then(res => setData(res));
return(
{data ? <div>{data}</div> : <div>Loading...</div>}
)
}
// with useEffect
const MyComponent = () => {
[data, setData] = useState();
useEffect(() => {
fetchDataFromAPI().then(res => setData(res))
}, []);
return(
{data ? <div>{data}</div> : <div>Loading...</div>}
)
}
Is there an advantage (performance-wise or other) to useEffect in such usecases ?
I. Cleanup
What if your component gets destroyed before the fetch is completed? You get an error.
useEffect gives you an easy way to cleanup in handler's return value.
II. Reactions to prop change.
What if you have a userId passed in a props that you use to fetch data. Without useEffect you'll have to duplicate userId in the state to be able to tell if it changed so that you can fetch the new data.
The thing is, useEffect is not executed on every render.
To see this more clearly, let's suppose that your component MyComponent is being rendered by a parent component (let's call it ParentComponent) and it receives a prop from that parent component that can change from a user action.
ParentComponent
const ParentComponent = () => {
const [ counter, setCounter ] = useState(0);
const onButtonClicked = () => setCounter(counter + 1);
return (
<>
<button onClick={onButtonClicked}>Click me!</button>
<MyComponent counter={counter} />
</>
);
}
And your MyComponent (slightly modified to read and use counter prop):
const MyComponent = ({ counter }) => {
[data, setData] = useState();
useEffect(() => {
fetchDataFromAPI().then(res => setData(res))
}, []);
return(
<div>
<div>{counter}</div>
{data ? <div>{data}</div> : <div>Loading...</div>}
</div>
)
}
Now, when the component MyComponent is mounted for the first time, the fetch operation will be performed. If later the user clicks on the button and the counter is increased, the useEffect will not be executed (but the MyComponent function will be called in order to update due to counter having changed)!
If you don't use useEffect, when the user clicks on the button, the fetch operation will be executed again, since the counter prop has changed and the render method of MyComponent is executed.
useEffect is handling the side effect of the problem. useEffect is the combination of componentDidMount and componentDidUpdate. every initial render and whenever props updated it will be executed.
For an exmaple:
useEffect(() => {
fetchDataFromAPI().then(res => setData(res))
}, []);
Another example:
let's assume you have multiple state variables, the component will re-render for every state values change. But We may need to run useEffect in a specific scenario, rather than executing it for each state change.
function SimpleUseEffect() {
let [userCount, setUserCount] = useState(0);
let [simpleCount, setSimpleCount] = useState(0);
useEffect(() => {
alert("Component User Count Updated...");
}, [userCount]);
useEffect(() => {
alert("Component Simple Count Updated");
}, [simpleCount]);
return (
<div>
<b>User Count: {userCount}</b>
<b>Simple Count: {simpleCount}</b>
<input type="button" onClick={() => setUserCount(userCount + 1}} value="Add Employee" />
<input type="button" onClick={() => setSimpleCount(simpleCount + 1}} value="Update Simple Count" />
</div>
)
}
In the above code whenever your props request changed, fetchDataFromAPI executes and updated the response data. If you don't use useEffect, You need to automatically handle all type of side effects.
Making asynchronous API calls for data
Setting a subscription to an observable
Manually updating the DOM element
Updating global variables from inside a function
for more details see this blog https://medium.com/better-programming/https-medium-com-mayank-gupta-6-88-react-useeffect-hooks-in-action-2da971cfe83f
I'm running lint with my React app, and I receive this error:
error JSX props should not use arrow functions react/jsx-no-bind
And this is where I'm running the arrow function (inside onClick):
{this.state.photos.map(tile => (
<span key={tile.img}>
<Checkbox
defaultChecked={tile.checked}
onCheck={() => this.selectPicture(tile)}
style={{position: 'absolute', zIndex: 99, padding: 5, backgroundColor: 'rgba(255, 255, 255, 0.72)'}}
/>
<GridTile
title={tile.title}
subtitle={<span>by <b>{tile.author}</b></span>}
actionIcon={<IconButton onClick={() => this.handleDelete(tile)}><Delete color="white"/></IconButton>}
>
<img onClick={() => this.handleOpen(tile.img)} src={tile.img} style={{cursor: 'pointer'}}/>
</GridTile>
</span>
))}
Is this a bad practice that should be avoided? And what's the best way to do it?
Why you shouldn't use inline arrow functions in JSX props
Using arrow functions or binding in JSX is a bad practice that hurts performance, because the function is recreated on each render.
Whenever a function is created, the previous function is garbage collected. Rerendering many elements might create jank in animations.
Using an inline arrow function will cause PureComponents, and components that use shallowCompare in the shouldComponentUpdate method to rerender anyway. Since the arrow function prop is recreated each time, the shallow compare will identify it as a change to a prop, and the component will rerender.
As you can see in the following 2 examples - when we use inline arrow function, the <Button> component is rerendered each time (the console shows the 'render button' text).
Example 1 - PureComponent without inline handler
class Button extends React.PureComponent {
render() {
const { onClick } = this.props;
console.log('render button');
return (
<button onClick={ onClick }>Click</button>
);
}
}
class Parent extends React.Component {
state = {
counter: 0
}
onClick = () => this.setState((prevState) => ({
counter: prevState.counter + 1
}));
render() {
const { counter } = this.state;
return (
<div>
<Button onClick={ this.onClick } />
<div>{ counter }</div>
</div>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Example 2 - PureComponent with inline handler
class Button extends React.PureComponent {
render() {
const { onClick } = this.props;
console.log('render button');
return (
<button onClick={ onClick }>Click</button>
);
}
}
class Parent extends React.Component {
state = {
counter: 0
}
render() {
const { counter } = this.state;
return (
<div>
<Button onClick={ () => this.setState((prevState) => ({
counter: prevState.counter + 1
})) } />
<div>{ counter }</div>
</div>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Binding methods to this without inlining arrow functions
Binding the method manually in the constructor:
class Button extends React.Component {
constructor(props, context) {
super(props, context);
this.cb = this.cb.bind(this);
}
cb() {
}
render() {
return (
<button onClick={ this.cb }>Click</button>
);
}
}
Binding a method using the proposal-class-fields with an arrow function. As this is a stage 3 proposal, you'll need to add the Stage 3 preset or the Class properties transform to your babel configuration.
class Button extends React.Component {
cb = () => { // the class property is initialized with an arrow function that binds this to the class
}
render() {
return (
<button onClick={ this.cb }>Click</button>
);
}
}
Function Components with inner callbacks
When we create an inner function (event handler for example) inside a function component, the function will be recreated every time the component is rendered. If the function is passed as props (or via context) to a child component (Button in this case), that child will re-render as well.
Example 1 - Function Component with an inner callback:
const { memo, useState } = React;
const Button = memo(({ onClick }) => console.log('render button') || (
<button onClick={onClick}>Click</button>
));
const Parent = () => {
const [counter, setCounter] = useState(0);
const increment = () => setCounter(counter => counter + 1); // the function is recreated all the time
return (
<div>
<Button onClick={increment} />
<div>{counter}</div>
</div>
);
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
To solve this problem, we can wrap the callback with the useCallback() hook, and set the dependencies to an empty array.
Note: the useState generated function accepts an updater function, that provides the current state. In this way, we don't need to set the current state a dependency of useCallback.
Example 2 - Function Component with an inner callback wrapped with useCallback:
const { memo, useState, useCallback } = React;
const Button = memo(({ onClick }) => console.log('render button') || (
<button onClick={onClick}>Click</button>
));
const Parent = () => {
const [counter, setCounter] = useState(0);
const increment = useCallback(() => setCounter(counter => counter + 1), []);
return (
<div>
<Button onClick={increment} />
<div>{counter}</div>
</div>
);
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
Using inline functions like this is perfectly fine. The linting rule is outdated.
This rule is from a time when arrow functions were not as common and people used .bind(this), which used to be slow. The performance issue has been fixed in Chrome 49.
Do pay attention that you do not pass inline functions as props to a child component.
Ryan Florence, the author of React Router, has written a great piece about this:
https://reacttraining.com/blog/react-inline-functions-and-performance/
This is because an arrow function apparently will create a new instance of the function on each render if used in a JSX property. This might create a huge strain on the garbage collector and will also hinder the browser from optimizing any "hot paths" since functions will be thrown away instead of reused.
You can see the whole explanation and some more info at https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md
Why shouldn't JSX props use arrow functions or bind?
Mostly, because inline functions can break memoization of optimized components:
Traditionally, performance concerns around inline functions in React have been related to how passing new callbacks on each render breaks shouldComponentUpdate optimizations in child components. (docs)
It is less about additional function creation cost:
Performance issues with Function.prototype.bind got fixed here and arrow functions are either a native thing or are transpiled by babel to plain functions; in both cases we can assume it’s not slow. (React Training)
I believe people claiming function creation is expensive have always been misinformed (React team never said this). (Tweet)
When is the react/jsx-no-bind rule useful?
You want to ensure, that memoized components work as intended:
React.memo (for function components)
PureComponent or custom shouldComponentUpdate (for class components)
By obeying to this rule, stable function object references are passed. So above components can optimize performance by preventing re-renders, when previous props have not changed.
How to solve the ESLint error?
Classes: Define the handler as method, or class property for this binding.
Hooks: Use useCallback.
Middleground
In many cases, inline functions are very convenient to use and absolutely fine in terms of performance requirements. Unfortunately, this rule cannot be limited to only memoized component types. If you still want to use it across-the-board, you could e.g. disable it for simple DOM nodes:
rules: {
"react/jsx-no-bind": [ "error", { "ignoreDOMComponents": true } ],
}
const Comp = () => <span onClick={() => console.log("Hello!")} />; // no warning
To avoid creating new functions with the same arguments, you could memoize the function bind result, here is a simple utility named memobind to do it: https://github.com/supnate/memobind
You can remove this error by wrapping the function inside useCallback.
For those wondering when you need to pass data in the callback. Ex.:
const list = todos.map((todo, index) => (
<Todo
onClick={() => { onTodoClick(todo.id, todo.title, index) }
/>
));
Solution
According to the official documentation, you should do:
Move the function arguments into the children component:
const list = todos.map((todo, index) => (
<Todo
onClick={onTodoClick}
todoId={todo.id}
todoTitle={todo.title}
indexOnList={index}
/>
));
In the children component (<Todo />), pass the arguments in the call:
function Todo(props) {
// component properties
const { onClick, todoId, todoTitle, indexOnList } = props;
// we move the call from the parent to the children
const handleClick = useCallback(() => {
onClick(todoId, todoTitle, indexOnList);
}, [todoId, todoTitle, indexOnList]);
return (
<div onClick={handleClick}>
{/* the rest of the component remains the same */}
</div>
);
}
Is this the best solution?
I dislike this solution. You end up with parent's data and logic in the children component. This makes the children component dependent on the parent component, breaking the dependency rule.
That's a big no-no for me.
What I do is just disable this rule. According to Ryan Florence (React Router author), this is not a big deal anyway:
https://medium.com/#ryanflorence/react-inline-functions-and-performance-bdff784f5578
The new (in beta, jan 2023) React tutorial uses both functions and arrow functions as JSX props. This hints strongly at this not being a major concern.
You can use arrow functions using react-cached-handler library, no need to be worried about re-rendering performance :
Note : Internally it caches your arrow functions by the specified key,
no need to be worried about re-rendering!
render() {
return (
<div>
{this.props.photos.map((photo) => (
<Photo
key={photo.url}
onClick={this.handler(photo.url, (url) => {
console.log(url);
})}
/>
))}
</div>
);
}
Other features:
Named handlers
Handle events by arrow functions
Access to the key, custom arguments and the original event
Component rendering performace
Custom context for handlers
You may also see this this error if the function you are using in your onClick handler is a regular (non-inline) function defined outside of the render method but using the function keyword.
Declare your handler function as an arrow function using const, outside of you render method, and React will stop complaining...