Questions about useCallback hook and anonymous function - javascript

When passing a callback function, especially when passing a parameterized function, I know that I should use the useCallback hook because the use of anonymous functions can adversely affect performance.
the example of anonymous function I said is like this.
import React, { useState } from 'react';
const Component = () => {
const [param, setParam] = useState('');
...
return (
...
<SomeComponent
onClick={() => setParam('parameter')}
{...others}
/>
);
}
In the process of converting an anonymous function to use this hook, I encountered an error saying 'Too many renders' or it didn't work properly.
But I don't know exactly in what situation and in what situation.
and I used useCallback like below.
import React, { useState, useCallback } from 'react';
const Component = () => {
const [param, setParam] = useState('');
const handleClick = useCallback((params) => {
setParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
);
}
However, when using an anonymous function to return within useCallback, it also worked.
This means code like here. (Only the differences compared to the code above.)
const handleClick = useCallback((params) => {
return () => setParam(params);
},[]);
In this situation, I wonder if it's worse than just using an anonymous function inside the useCallback if I simply use an anonymous function instead of using this hook.

const handleClick = useCallback((params) => {
setParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
);
in the above code during first render, at this statement "onClick={handleClick('parameter')}" handleClick function is called with a string named "parameter". since handleClick has setParam("parameter"), it will update state. updating state will cause re-render which will again come to same statement "onClick={handleClick('parameter')}" causing infinte loop.
following code you added later works because you are not updating state, instead returning a function, which acts as onclick handler.
const handleClick = useCallback((params) => {
return () => setParam(params);
},[]);
better way of doing this shoud be as follwing,
import React, { useState, useCallback } from 'react';
const Component = () => {
const [param, setParam] = useState('');
const handleClick = useCallback((params) => {
setParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick}
{...others}
/>
);
}
coming back to your question , comparing performance depends on the other function definitions and render times of child compoents inside return function inside your Compoenent.
let's say you have another onclickHanldier named 'anotherHandleClick' inside your app.
then your component looks like this
const Component = () => {
const [param, setParam] = useState('');
const [anotherParam, setAnotherParam] = useState('');
const handleClick = (params) => {
setParam(params);
};
const anotherHandleClick =(params) => {
setAnotherParam(params);
};
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
<SomeComponent
onClick={antherHandleClick('parameter')}
{...others}
/>
);
}
in the above component when any one of "SomeCompoenent" clicked entiere "Component" re-renders, so the handler functions are newly defined.and when both 's does referential equality check on onclick handler functions, they believe it is new handler function which casues them to render both.
in that case it is better to use useCallBack hook as following,
const Component = () => {
const [param, setParam] = useState('');
const [anotherParam, setAnotherParam] = useState('');
const handleClick = useCallback((params) => {
setParam(params);
},[]);
const anotherHandleClick = useCallback((params) => {
setAnotherParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
<SomeComponent
onClick={antherHandleClick('parameter')}
{...others}
/>
);
}
in the above code when any one is clicked , state changes. then when rendering, useCallback make sure that onclick handler refernces didn't change. so having dependency of onclick handlers won't be rerendered.
so final thought is It's creating a function on each render in both cases. the second (because it'e wrapped in a useCallback) will return the memoized function created on the initial render
when to use useMemo or useCallback refer this

In your code:
const handleClick = useCallback((params) => {
setParam(params);
},[]);
You should pass the parameters into the second argment of useCallback like so:
const handleClick = useCallback((params) => {
setParam(params);
},[setParam,param, SETPARAMACTUALVALUE]);
// Change SETPARAMACTUALVALUE to whatever the variable name is for the `setParam` hook

useCallback hook usage will be better if you want to save the function until hook's dependency changed. It gives you better performance because the hook remember internal function.

Related

How can I pass arguments to event listeners in react?

I am trying to make my code clean and readable. So I decided to create a custom hook to store my useStates there. Then I created a new file to store my event listener. Now, I have three files: Page.js, useStates.js, and Listeners.js.
The problem is that I cannot use my states in event listeners.
I tried to store my states in global scope variables in useStates.js and pass them with getters. But it didn't work because updating the state didn't change the page(But it did rerender).
useStates.js:
import react, { useState } from 'react';
export default () => {
const [myState, setMyState] = useState(false);
return { myState, setMyState };
}
Page.js:
import react from 'react';
import useStates from './useStates';
import { someActionListener } from './listeners';
export default () => {
const states = useStates();
return <SomeComponent
somProp={states.myState}
onSomeAction={ someActionListener } />
}
Listeners.js:
export const someActionListener = (e) => {
// This should be done
states.setMyState(!states.myState);
}
You could pass the states into the listener:
export const someActionListener = (e, states) => {
// This should be done
states.setMyState(!states.myState);
}
...
<SomeComponent
somProp={states.myState}
onSomeAction={e => someActionListener(e, states)}
/>
Or curry the states value instead, leading to a more clean UI, it saves the anonymous function callback:
export const someActionListener = states => e => {
// This should be done
states.setMyState(!states.myState);
}
...
<SomeComponent
somProp={states.myState}
onSomeAction={someActionListener(states)}
/>
I suggest tweaking the action listener to take a callback instead. Event listeners typically do this anyway. A curried function is an easy way to close over a callback and return the event listener callback. This decouples the state and state updating from the event listener logic, allowing the React component using both to maintain control of the state and how it's updated.
export const someActionListener = (callback) => (e) => {
// This should be done
callback();
}
Pass in the state updater function as a callback.
export default () => {
const states = useStates();
return (
<SomeComponent
somProp={states.myState}
onSomeAction={someActionListener(
() => states.setMyState(!states.myState))
)}
/>
);
}
Or using a functional state update since you are toggling a state value. This is to avoid stale state enclosures in callbacks.
<SomeComponent
somProp={states.myState}
onSomeAction={someActionListener(
() => states.setMyState(states => ({
...states
myState: !states.myState
})
}
/>

Browser freezing when using React Hook

When I enter into a router that refers to this Component, my browser just crashes. I've made some console tests and notices that when the response.data.message is up, it continually re-renders the page. Can someone help me?
import React from 'react'
import "./UsernameStory.css";
import Axios from "axios";
const UsernameStory = ({match}) => {
const [statue , setStatue] = React.useState("");
const [stories , setStories] = React.useState([]);
const fetchUsername = () => {
const urls = match.params.username;
Axios.post("http://localhost:8080/"+urls, {
}).then((response) => {
if(response.data.statue)
{
setStatue(response.data.statue);
}
if(response.data.message){
setStories(response.data.message);
}
})
}
return (
<div>
{fetchUsername()}
<p>{statue}</p>
<ul>
{stories.map((story , key) => (<li key={key}>{story.username}</li>))}
</ul>
</div>
)
}
export default UsernameStory
On every render, fetchUsername() is called and results in updating statue and stories, which results in another rerender, and thus leads to an infinite loop of rerendering (since every render triggers a state update).
A better practice for handling functions with side-effects like fetching data is to put the fetchUsername in useEffect.
const UsernameStory = ({match}) => {
const [statue , setStatue] = React.useState("");
const [stories , setStories] = React.useState([]);
useEffect(() => {
const urls = match.params.username;
Axios.post("http://localhost:8080/"+urls, {})
.then((response) => {
if(response.data.statue)
{
setStatue(response.data.statue);
}
if(response.data.message){
setStories(response.data.message);
}
});
}, []); // An empty denpendency array allows you to fetch the data once on mount
return (
<div>
<p>{statue}</p>
<ul>
{stories.map((story , key) => (<li key={key}>{story.username}</li>))}
</ul>
</div>
)
}
export default UsernameStory
You can't call function fetchUsername() inside return statement. It
will go in infinite loop.
You can't directly set the state of two variable as setStatue(response.data.statue) & setStories(response.data.message) respectively
use the useEffect hooks and set the status inside that hooks with conditional rendering to avoid looping.
call the fetchUsername() just before the return statement.
I looked at your code carefully. As you can see in this code, the fetchUsername() function is executed when the component is first rendered.
When you call setState() in the fetchUsername function, your component's state variable is updated. The problem is that when the state variable is updated, the component is rendered again. Then the fetchUsername function will be called again, right?
Try the useEffect Hook.
The example code is attached.
eg code
How about trying to change the way you call the fetchUsername() function with the useEffect hook instead?
import React, { useEffect } from 'react'
import "./UsernameStory.css";
import Axios from "axios";
const UsernameStory = ({match}) => {
const [statue , setStatue] = React.useState("");
const [stories , setStories] = React.useState([]);
useEffect(() => {
const fetchUsername = () => {
const urls = match.params.username;
Axios.post("http://localhost:8080/"+urls, {
}).then((response) => {
if(response.data.statue)
{
setStatue(response.data.statue);
}
if(response.data.message){
setStories(response.data.message);
}
})
}
fetchUsername();
// clean up here
return () => {
// something you wanna do when this component being unmounted
// console.log('unmounted')
}
}, [])
return (
<div>
<p>{statue}</p>
<ul>
{stories.map((story , key) => (<li key={key}>{story.username}</li>))}
</ul>
</div>
)
}
export default UsernameStory
But, sometimes your code just doesn't need a cleanup in some scenarios, you could read more about this here: Using the Effect Hook - React Documentation.

call function from useEffect and other function

i have a child component which emits an action to the parent component with a event:
Child component:
export default function ChildComponent(props) {
const classes = useStyles();
const [value, setValue] = React.useState([0, 5]);
const handleChange = (_, newValue) => {
setValue(newValue);
props.updateData(newValue)
};
return (
<div className={classes.root}>
<GrandSonComponent
value={value}
onChange={handleChange}
/>
</div>
);
}
Parent component:
export const ParentComponent = () => {
const [loading, setLoading] = React.useState(true);
const { appData, appDispatch } = React.useContext(Context);
function fetchElements(val) {
fetchData(val);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => { return fetchData() }, []);
async function fetchData(params) {
const res = await axios.get('/url', { params });
appDispatch({ type: "LOAD_ELEMENTS", elements: res.data });
}
return (
<div>
<ChildComponent updateData={fetchElements} />
<div>
.
.
.
)
};
i would like to know how to remove this line // eslint-disable-next-line react-hooks/exhaustive-deps
I need to add this line, because otherwise i see the eslint error:
React Hook useEffect has a missing dependency: 'fetchData'. Either include it or remove the
dependency array.eslintreact-hooks/exhaustive-deps
i need to use fetchData(params) function for the first time page is rendered and when user change/click value of child component with no eslit warnings!
Thanks!
First of all, you don't need to return the result of calling fetchData() function inside the useEffect hook.
Now coming to your problem, reason why you get a warning is because missing the dependencies of the useEffect could lead to bugs due to closures. React recommends that you don't omit any dependencies of the useEffect hook, useCallback hook, etc.
This sometimes leads to infinite loop of state update and re-render but that can be prevented by either using useCallback hook or other ways that could prevent useEffect hook from executing after each re-render of the component.
In your case, you could fix the problem by following the steps mentioned below:
Wrapping fetchData function in a useCallback hook
const fetchData = useCallback(async (params) => {
const res = await axios.get('/url', { params });
appDispatch({ type: "LOAD_ELEMENTS", elements: res.data });
}, []);
Add fetchData in the dependency array of the useEffect hook
useEffect(() => {
fetchData();
}, [fetchData]);

How to use a prop function inside of UseEffect?

I want to call a prop function inside of UseEffect. The following code works:
useEffect(() => {
props.handleClick(id);
}, [id]);
But lint is complaining about props not being a dependency.
If I do this, then the code no longer works and I have maximum re-render error:
useEffect(() => {
props.handleClick(id);
}, [id, props]);
How can I fix the lint issue?
Sample code:
Parent Component
const ParentGrid = ({rows, columns}) => {
const [selection, setSelection] = useState(null);
const handleClick = selectedRows => {
setSelection(selectedRows.map(i => rows[i]));
};
return (
<ChildGrid
columns={columns}
data={data}
handleClick={handleClick}
/>
Child Component
const ChildGrid = props => {
const {data, handleClick, ...rest} = props;
useEffect(() => {
handleClick(selectedRows);
}, [selectedRows]);
I see alot of weird and incorrect answers here so I felt the need to write this.
The reason you are reaching maximum call depth when you add the function to your dependency array is because it does not have a stable identity. In react, functions are recreated on every render if you do not wrap it in a useCallback hook. This will cause react to see your function as changed on every render, thus calling your useEffect function every time.
One solution is to wrap the function in useCallback where it is defined in the parent component and add it to the dependency array in the child component. Using your example this would be:
const ParentGrid = ({rows, columns}) => {
const [selection, setSelection] = useState(null);
const handleClick = useCallback((selectedRows) => {
setSelection(selectedRows.map(i => rows[i]));
}, [rows]); // setSelection not needed because react guarantees it's stable
return (
<ChildGrid
columns={columns}
data={data}
handleClick={handleClick}
/>
);
}
const ChildGrid = props => {
const {data, handleClick, ...rest} = props;
useEffect(() => {
handleClick(selectedRows);
}, [selectedRows, handleClick]);
};
(This assumes the rows props in parent component does not change on every render)
One correct way to do this is to add props.handleClick as a dependency and memoize handleClick on the parent (useCallback) so that the function reference does not change unnecessarily between re-renders.
It is generally NOT advisable to switch off the lint rule as it can help with subtle bugs (current and future)
in your case, if you exclude handleClick from deps array, and on the parent the function was dependent on parent prop or state, your useEffect will not fire when that prop or state on the parent changes, although it should, because the handleClick function has now changed.
you should destructure handleClick outside of props
at the start of the component you probably have something like this:
const myComponent = (props) =>
change to
const myComponent = ({ handleClick, id }) basically you can pull out any props you know as their actual name
then use below like so:
useEffect(() => {
handleClick(id);
}, [id, handleClick]);
or probably you don't actually need the function as a dependency so this should work
useEffect(() => {
handleClick(id);
}, [id]);
Add props.handleClick as the dependency
useEffect(() => {
props.handleClick(id);
}, [id, props.handleClick]);

Wrong React hooks behaviour with event listener

I'm playing around with React Hooks and am facing a problem.
It shows the wrong state when I'm trying to console log it using a button handled by event listener.
CodeSandbox: https://codesandbox.io/s/lrxw1wr97m
Click on 'Add card' button 2 times
In first card, click on Button1 and see in console that there are 2 cards in state (correct behaviour)
In first card, click on Button2 (handled by event listener) and see in console that there is only 1 card in state (wrong behaviour)
Why does it show the wrong state?
In first card, Button2 should display 2 cards in the console. Any ideas?
const { useState, useContext, useRef, useEffect } = React;
const CardsContext = React.createContext();
const CardsProvider = props => {
const [cards, setCards] = useState([]);
const addCard = () => {
const id = cards.length;
setCards([...cards, { id: id, json: {} }]);
};
const handleCardClick = id => console.log(cards);
const handleButtonClick = id => console.log(cards);
return (
<CardsContext.Provider
value={{ cards, addCard, handleCardClick, handleButtonClick }}
>
{props.children}
</CardsContext.Provider>
);
};
function App() {
const { cards, addCard, handleCardClick, handleButtonClick } = useContext(
CardsContext
);
return (
<div className="App">
<button onClick={addCard}>Add card</button>
{cards.map((card, index) => (
<Card
key={card.id}
id={card.id}
handleCardClick={() => handleCardClick(card.id)}
handleButtonClick={() => handleButtonClick(card.id)}
/>
))}
</div>
);
}
function Card(props) {
const ref = useRef();
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
}, []);
return (
<div className="card">
Card {props.id}
<div>
<button onClick={props.handleButtonClick}>Button1</button>
<button ref={node => (ref.current = node)}>Button2</button>
</div>
</div>
);
}
ReactDOM.render(
<CardsProvider>
<App />
</CardsProvider>,
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>
I am using React 16.7.0-alpha.0 and Chrome 70.0.3538.110
BTW, if I rewrite the CardsProvider using a сlass, the problem is gone.
CodeSandbox using class: https://codesandbox.io/s/w2nn3mq9vl
This is a common problem for functional components that use the useState hook. The same concerns are applicable to any callback functions where useState state is used, e.g. setTimeout or setInterval timer functions.
Event handlers are treated differently in CardsProvider and Card components.
handleCardClick and handleButtonClick used in the CardsProvider functional component are defined in its scope. There are new functions each time it runs, they refer to cards state that was obtained at the moment when they were defined. Event handlers are re-registered each time the CardsProvider component is rendered.
handleCardClick used in the Card functional component is received as a prop and registered once on component mount with useEffect. It's the same function during the entire component lifespan and refers to stale state that was fresh at the time when the handleCardClick function was defined the first time. handleButtonClick is received as a prop and re-registered on each Card render, it's a new function each time and refers to fresh state.
Mutable state
A common approach that addresses this problem is to use useRef instead of useState. A ref is basically a recipe that provides a mutable object that can be passed by reference:
const ref = useRef(0);
function eventListener() {
ref.current++;
}
In this case a component should be re-rendered on a state update like it's expected from useState, refs aren't applicable.
It's possible to keep state updates and mutable state separately but forceUpdate is considered an anti-pattern in both class and function components (listed for reference only):
const useForceUpdate = () => {
const [, setState] = useState();
return () => setState({});
}
const ref = useRef(0);
const forceUpdate = useForceUpdate();
function eventListener() {
ref.current++;
forceUpdate();
}
State updater function
One solution is to use a state updater function that receives fresh state instead of stale state from the enclosing scope:
function eventListener() {
// doesn't matter how often the listener is registered
setState(freshState => freshState + 1);
}
In this case a state is needed for synchronous side effects like console.log, a workaround is to return the same state to prevent an update.
function eventListener() {
setState(freshState => {
console.log(freshState);
return freshState;
});
}
useEffect(() => {
// register eventListener once
return () => {
// unregister eventListener once
};
}, []);
This doesn't work well with asynchronous side effects, notably async functions.
Manual event listener re-registration
Another solution is to re-register the event listener every time, so a callback always gets fresh state from the enclosing scope:
function eventListener() {
console.log(state);
}
useEffect(() => {
// register eventListener on each state update
return () => {
// unregister eventListener
};
}, [state]);
Built-in event handling
Unless the event listener is registered on document, window or other event targets that are outside of the scope of the current component, React's own DOM event handling has to be used where possible, this eliminates the need for useEffect:
<button onClick={eventListener} />
In the last case the event listener can be additionally memoized with useMemo or useCallback to prevent unnecessary re-renders when it's passed as a prop:
const eventListener = useCallback(() => {
console.log(state);
}, [state]);
Previous edition of this answer suggested to use mutable state that was applicable to initial useState hook implementation in React 16.7.0-alpha version but isn't workable in final React 16.8 implementation. useState currently supports only immutable state.*
A much cleaner way to work around this is to create a hook I call useStateRef
function useStateRef(initialValue) {
const [value, setValue] = useState(initialValue);
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
You can now use the ref as a reference to the state value.
Short answer for me was that useState has a simple solution for this:
function Example() {
const [state, setState] = useState(initialState);
function update(updates) {
// this might be stale
setState({...state, ...updates});
// but you can pass setState a function instead
setState(currentState => ({...currentState, ...updates}));
}
//...
}
Short answer for me
this WILL NOT not trigger re-render ever time myvar changes.
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, []);
This WILL trigger render -> putting myvar in []
const [myvar, setMyvar] = useState('')
useEffect(() => {
setMyvar('foo')
}, [myvar]);
Check the console and you'll get the answer:
React Hook useEffect has a missing dependency: 'props.handleCardClick'. Either include it or remove the dependency array. (react-hooks/exhaustive-deps)
Just add props.handleCardClick to the array of dependencies and it will work correctly.
This way your callback will have updated state values always ;)
// registers an event listener to component parent
React.useEffect(() => {
const parentNode = elementRef.current.parentNode
parentNode.addEventListener('mouseleave', handleAutoClose)
return () => {
parentNode.removeEventListener('mouseleave', handleAutoClose)
}
}, [handleAutoClose])
To build off of Moses Gitau's great answer, if you are developing in Typescript, to resolve type errors make the hook function generic:
function useStateRef<T>(initialValue: T | (() => T)):
[T, React.Dispatch<React.SetStateAction<T>>, React.MutableRefObject<T>] {
const [value, setValue] = React.useState(initialValue);
const ref = React.useRef(value);
React.useEffect(() => {
ref.current = value;
}, [value]);
return [value, setValue, ref];
}
Starting from the answer of #Moses Gitau, I'm using a sligthly different one that doesn't give access to a "delayed" version of the value (which is an issue for me) and is a bit more minimalist:
import { useState, useRef } from 'react';
function useStateRef(initialValue) {
const [, setValueState] = useState(initialValue);
const ref = useRef(initialValue);
const setValue = (val) => {
ref.current = val;
setValueState(val); // to trigger the refresh
};
const getValue = (val) => {
return ref.current;
};
return [getValue , setValue];
}
export default useStateRef;
This is what I'm using
Example of usage :
const [getValue , setValue] = useStateRef(0);
const listener = (event) => {
setValue(getValue() + 1);
};
useEffect(() => {
window.addEventListener('keyup', listener);
return () => {
window.removeEventListener('keyup', listener);
};
}, []);
Edit : It now gives getValue and not the reference itself. I find it better to keep things more encapsulated in that case.
after changing the following line in the index.js file the button2 works well:
useEffect(() => {
ref.current.addEventListener("click", props.handleCardClick);
return () => {
ref.current.removeEventListener("click", props.handleCardClick);
};
- }, []);
+ });
you should not use [] as 2nd argument useEffect unless you want it to run once.
more details: https://reactjs.org/docs/hooks-effect.html

Categories

Resources