I've been using React for a couple years, with functional components and hooks. The most frustrating thing I've found has to do with passing callbacks. I have two concerns:
If an element receives a callback from a parent it can't really use that function in a event listener or onEvent field or in a setTimeout/setInterval, because the passed function may be redefined any time the parent rerenders.
A function that's generated at each render (like an arrow function) and passed to a child element breaks React.memo().
I've seen some limited solutions to this, such as useEventListener() but not one that addresses all the situations that concern me.
So I've written my own hook that I've called useHandler. Unlike useCallback, which generates a new function whenever the inputs change, useHandler always returns the same function. This way the function case be passed to an element using React.memo() or to a setInterval or whatever.
It call be used by the parent element (needed for the React.memo case) or by the child element (if you're worried that a passed-in function might change). Here's an example of it used in the parent:
const onClick = useHandler(event => {
//Do whatever you want. Reference useState variables or other values that might change,
})
<MyElement onClick={onClick} />
MyElement now always gets the same function, but calling that function will execute the most recent code in the parent element.
Here's an example in the child element:
const reportBack = useHandler(props.reportBack)
useEffect(() => {
setInterval(reportBack, 1000)
}, [reportBack])
The interval callback will always call the current reportBack passed from the parent.
The only thing missing for me is that I haven't modified the React eslint config to recognize that that results of useHandler cannot change and thus shouldn't generate a warning if omitted from the dependency list in useEffect or similar.
And finally, here's the source for my onHandler hook:
export function useHandler(callback) {
const callbackRef = useRef()
callbackRef.current = callback
return useRef((...args) => callbackRef.current(...args)).current
}
My questions are: Are there any flaws in this solution? Is it a good way to address my issues with React callbacks? I haven't found anything more elegant but I'm plenty willing to believe that I've missed something.
Thanks.
I can't comment on the second point as I've not used React.memo but the first point is why we have cleanup functions.
Let's say you have a function that is passed as a prop down to your component and you want to use it as an event listener.
If you are attaching this listener inside a useEffect like: window.addEventListener('someEvent', (event) => someFunctionPassedAsProp(event))
Then the useEffect can look something like:
useEffect(() => {
const handler = (event) => someFunctionPassedAsProp(event)
window.addEventListener('someEvent', handler)
return () => window.removeEventListener('someEvent', handler)
}, [someFunctionPassedAsProp])
Similarly for intervals you can do something like:
useEffect(() => {
const interval = setInterval(() => someFunctionPassedAsProp(), 1000)
return () => clearInterval(interval)
}, [someFunctionPassedAsProp])
Related
In react, we can memoize a function using useCallback so the function doesn't change every rerender
const fn = useCallback(someOtherFn, []);
Instead, can we define someOtherFn outside the component, and if it uses setState, we give that to it as an argument?
Something like
function someOtherFn(setState) {
setState("ok")
}
function myComponenet(){
......
}
Sorry in advance if this is a stupid question.
You can do that, but it might probably defeat the purpose, since you'd have to write another function that calls it, and that other function would be created every render. For example, perhaps you're thinking of:
function someOtherFn(setState) {
setState("ok")
}
const MyComponent = () => {
const [state, setState] = useState();
return <button onClick={() => someOtherFn(setState)}>click</button>;
};
Although someOtherFn indeed wouldn't need to be created every time - it'd only need to be created once - the handler function, the
() => someOtherFn(setState)
would be created every time the component renders, which could be a problem in certain uncommon (IMO) situations.
To avoid this issue, you would have to bind the setState argument to the function persistently somehow - which would be most easily accomplished through the use useCallback.
What's the different between useEffect when you pass it dependencies as the second parameter and useCallback?
Don't both essentially run the function/code passed as the first parameter whenever the dependencies passed as the second parameter change?
From what I've read the two hooks are intended to serve different purposes, but my question is whether they in actuality could be used interchangeably because they functionally do the same thing
They're too different.
useEffect will run the function inside when the dependency array changes.
useCallback will create a new function when the dependency array changes.
You can't switch useEffect with useCallback alone because you also need the logic to run the newly created function. (I suppose you could implement this if you used a ref as well, but that'd be quite strange.)
You can't switch useCallback with useEffect because you very often don't want to run the newly created function immediately - rather, you usually want to pass it as a prop to some other component.
useCallback primarily exists for optimization purposes, to reduce re-renders of a child component.
No, They are not same.
useEffect - is used to run side effects in the component when something changes. useEffect does
not return you anything. It just runs a piece of code in the component.
useCallback - Whereas useCallback returns a function, it does not execute the code actually. It is important to understand that
functions are objects in Javascript. If you don't use useCallback, the function you define inside the component is
re-created whenever the component rebuilds.
Example
Consider this example, this component will go in a infinite loop. Think Why?
const TestComponent = props => {
const testFunction = () => {
// does something.
};
useEffect(() => {
testFunction();
// The effect calls testFunction, hence it should declare it as a dependency
// Otherwise, if something about testFunction changes (e.g. the data it uses), the effect would run the outdated version of testFunction
}, [testFunction]);
};
Because on each render the testFunction
would be re-created and we already know that ueEffect will run the code when ever the testFunction changes. And since testFunction changes on each render, the useEffect will keep on running, and hence an infinite loop.
To fix this, we have to tell react, hey please don't re-create the testFunction on each render, create it only on first render (or when something changes on which it depends).
const TestComponent = props => {
const testFunction = useCallback(() => {
// does something.
}, []);
useEffect(() => {
testFunction();
// The effect calls testFunction, hence it should declare it as a dependency
// Otherwise, if something about testFunction changes (e.g. the data it uses), the effect would run the outdated version of testFunction
}, [testFunction]);
};
This won't be a infinite loop, since instance of testFunction will change only on first render and hence useEffect will run only once.
useEffect will run the function inside when the dependency array changes.
useCallback will create a new function when the dependency array changes.
Let's take an example, If I run the below code and click the first button it'll always rerender MemoComponent as well. Why because every time
we are passing new onClick function to this. To avoid re-rendering of MemoComponent what we can do is wrap onClick to useCallback. Whenever you want to create a new function pass state to the dependence array.
If you want to perform some action on state change you can write inside useEffect.
const Button = ({ onClick }) => {
console.log("Render");
return <button onClick={onClick}>Click</button>;
};
const MemoComponent = React.memo(Button);
export default function Home() {
const [state, setState] = useState(1);
useEffect(() => {
console.log(state); // this will execute when state changes
}, [state]);
const onClick = () => {};
// const onClick = useCallback(() => {},[])
return (
<main>
<button onClick={() => setState(1 + state)}>{state}</button>
<MemoComponent onClick={onClick} />
</main>
);
}
useEffect
It's the alternative for the class component lifecycle methods componentDidMount, componentWillUnmount, componentDidUpdate, etc. You can also use it to create a side effect when dependencies change, i.e. "If some variable changes, do this".
Whenever you have some logic that is executed as reaction to a state change or before a change is about to happen.
useEffect(() => {
// execute when state changed
() => {
// execute before state is changed
}
}, [state]);
OR
useEffect(() => {
// execute when state changed
() => {
// execute before state is changed
}
}, []);
useCallback
On every render, everything that's inside a functional component will run again. If a child component has a dependency on a function from the parent component, the child will re-render every time the parent re-renders even if that function "doesn't change" (the reference changes, but what the function does won't).
It's used for optimization by avoiding unnecessary renders from the child, making the function change the reference only when dependencies change. You should use it when a function is a dependency of a side effect e.g. useEffect.
Whenever you have a function that is depending on certain states. This hook is for performance optimization and prevents a function inside your component to be reassigned unless the depending state is changed.
const myFunction = useCallback(() => {
// execute your logic for myFunction
}, [state]);
Without useCallback, myFunction will be reassigned on every render. Therefore it uses more compute time as it would with useCallback.
I am currently trying to learn about the inner workings of React in context of when a component is re-rendered or especially when (callback-)functions are recreated.
In doing so, I have come across a phenomenon which I just cannot get my head around. It (only) happens when having a state comprising an array. Here is a minimal code that shows the "problem":
import { useEffect, useState } from "react";
export function Child({ value, onChange }) {
const [internalValue, setInternalValue] = useState(value);
// ... stuff interacting with internalValue
useEffect(() => {
onChange(internalValue);
}, [onChange, internalValue]);
return <div>{value}</div>;
}
export default function App() {
const [state, setState] = useState([9.0]);
return <Child value={state[0]} onChange={(v) => setState([v])} />;
}
The example comprises a Parent (App) Component with a state, being an array of a single number, which is given to the Child component. The Child may do some inner workings and set the internal state with setInternalValue, which in turn will trigger the effect. This effect will raise the onChange function, updating a value of the state array of the parent. (Note that this example is minimized to show the effect. The array would have multiple values, where for each a Child component is shown) However this example results in an endless re-rendering of the Child with the following console warning being raised throughout:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Debugging shows, that the re-rendering occurs due to onChange being changed. However, I do not understand this. Why is onChange being changed? Neither internalState nor state is changed anywhere.
There are two workarounds I found:
Remove onChange from the dependencies of the effect in the Child. This "solves" the re-rendering and would be absolutely acceptable for my use case. However, it is bad practice as far as I know, since onChange is used inside the effect. Also, ESLint is indicating this as a warning.
Using a "raw" number in the state, instead of an array. This will also get rid of the re-rendering. However this is only acceptable in this minimal example, as there is only one number used. For a dynamic count of numbers, this workaround is not viable.
useCallback is also not helping and just "bubbling up" the re-recreation of the onChange function.
So my question is: Do React state (comprising arrays) updates are being handled differently and is omitting a dependency valid here? What is the correct way to do this?
Why is onChange being changed?
On every render, you create a new anonymous function (v) => setState([v]).
Since React makes a shallow comparison with the previous props before rendering, it always results in a render, since in Javascript:
const y = () => {}
const x = () => {}
x !== y // always true
// In your case
const onChangeFromPreviousRender = (v) => setState([v])
const onChangeInCurrentRender = (v) => setState([v])
onChangeFromPreviousRender !== onChangeInCurrentRender
What is the correct way to do this?
There are two ways to correct it, since setState is guaranteed to be stable, you can just pass the setter and use your logic in the component itself:
// state[0] is primitive
// setState stable
<Child value={state[0]} onChange={setState} />
useEffect(() => {
// set as array
onChange([internalValue]);
}, [onChange, internalValue]);
Or, Memoizing the function will guarantee the same identity.
const onChange = useCallback(v => setState([v]), []);
Notice that we memoize the function only because of its nontrivial use case (beware of premature optimization).
I have a customHook, and I need to call it in two places. One is in the top level of the component. The other place is in a onclick function of a button, this button is a refresh button which calls the customHook to fetch new data like below. I am thinking of two approaches:
create a state for the data, call hook and set the data state in the component and in the onclick function, call hook and set the data state. However, the hook cannot be called inside another function i.e onclick in this case.
create a boolean state called trigger, everytime onclick of the button, toggle the trigger state and pass the trigger state into the myCallback in the dependent list so that myCallback function gets recreated, and the hook gets called. However, I don't really need to use this trigger state inside the callback function, and the hook gives me error of removing unnecessary dependency. I really like this idea, but is there a way to overcome this issue?
Or is there any other approaches to achieve the goal?
const MyComponent = () => {
const myCallback = React.useCallback(() => { /*some post processing of the data*/ }, []);
const data = customHook(myCallback);
return <SomeComponent data={data}>
<button onclick={/*???*/}></button>
</SomeComponent>;
};
It is possible to make your second example work with some tweaking. Instead of passing in a dependency to update the effect function, just make the effect function a stand-alone function that you pass into useEffect, but can also call in other places (e.g. you can return the effect function from your hook so your hook users can use it too)
For example:
const App = () => {
const { resource, refreshResource } = useResource()
return (
<div>
<button onClick={refreshResource}>Refresh</button>
{resource || 'Loading...'}
</div>
)
}
const useResource = () => {
const [resource, setResource] = useState(null)
const refreshResource = async () => {
setResource(null)
setResource(await fetchResource())
}
useEffect(refreshResource, [])
return { resource, refreshResource }
}
const fetchResource = async () => {
await new Promise(resolve => setTimeout(resolve, 500))
return Math.random()
}
Edit
I hadn't realized that the hook couldn't be edited. I honestly can't think of any good solutions to your problem - maybe one doesn't exist. Ideally, the API providing this custom hook would also provide some lower-level bindings that you could use to get around this issue.
If worst comes to worst and you have to proceed with some hackish solution, your solution #2 of updating the callback should work (assuming the custom hook refetches the resource whenever the parameter changes). You just have to get around the linting rule, which, I'm pretty sure you can do with an /* eslint-disable-line */ comment on the line causing the issue, if eslint is being used. Worst comes to worst, you can make a noop function () => {} that you call with your trigger parameter - that should put the linter at bay.
So, I'm using hooks to manage the state of a set of forms, set up like so:
const [fieldValues, setFieldValues] = useState({}) // Nothing, at first
When setting the value, the state doesn't update:
const handleSetValues = values => {
const _fieldValues = {
...fieldValues,
...values
}
setFieldValues(_fieldValues)
console.log(fieldValues) // these won't be updated
setTimeout(() => {
console.log(fieldValues) // after ten seconds, it's still not updated
},10000)
}
If I call the function a second time, it'll have updated, but that's not gonna work for me.
I never saw behaviour like this in class components.
Is it meant to... like, not update? Or just update whenever it feels like it? Really confusing behaviour.
setFieldValues(_fieldValues) is an async call, means you won't able to get the result in the very next line of this.
You can use useEffect hook.
useEffect(() => {
// do your work here
}, [fieldValues]);
It seems from your question that you have background of Class components of React, so useEffect is similar to componentDidMount and componentDidUpdate lifecycle methods.
useEffect calls whenever the state in the dependency array (in your case [fieldValues]) changes and you get the updated value in useEffect body.
You can also perform componentWillUnmount work in useEffect as well.
Have a brief guide.
setFieldValues is an asynchronous function, so logging the value below the statement will not have any effect.
Regarding using setTimeout, the function would capture the current value of props being passed to it and hence that would be the value printed to the console. This is true to any JS function, see the snippet below:
function init(val) {
setTimeout(() => {
console.log(val);
}, 1000);
}
let counterVal = 1;
init(counterVal);
counterVal++;
So how can we print the values when the value changes? The easy mechanism is to use a useEffect:
useEffect(() => {
console.log(fieldValues)
}, [fieldValues]);