Why include external functions in dependency array in useEffect [duplicate] - javascript

I'm having a hard time understanding the 'exhaustive-deps' lint rule.
I already read this post and this post but I could not find an answer.
Here is a simple React component with the lint issue:
const MyCustomComponent = ({onChange}) => {
const [value, setValue] = useState('');
useEffect(() => {
onChange(value);
}, [value]);
return (
<input
value={value}
type='text'
onChange={(event) => setValue(event.target.value)}>
</input>
)
}
It requires me to add onChange to the useEffect dependencies array. But in my understanding onChange will never change, so it should not be there.
Usually I manage it like this:
const MyCustomComponent = ({onChange}) => {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
onChange(event.target.value)
}
return (
<input
value={value}
type='text'
onChange={handleChange}>
</input> ​
)
}
Why the lint? Any clear explanation about the lint rule for the first example?
Or should I not be using useEffect here? (I'm a noob with hooks)

The reason the linter rule wants onChange to go into the useEffect hook is because it's possible for onChange to change between renders, and the lint rule is intended to prevent that sort of "stale data" reference.
For example:
const MyParentComponent = () => {
const onChange = (value) => { console.log(value); }
return <MyCustomComponent onChange={onChange} />
}
Every single render of MyParentComponent will pass a different onChange function to MyCustomComponent.
In your specific case, you probably don't care: you only want to call onChange when the value changes, not when the onChange function changes. However, that's not clear from how you're using useEffect.
The root here is that your useEffect is somewhat unidiomatic.
useEffect is best used for side-effects, but here you're using it as a sort of "subscription" concept, like: "do X when Y changes". That does sort of work functionally, due to the mechanics of the deps array, (though in this case you're also calling onChange on initial render, which is probably unwanted), but it's not the intended purpose.
Calling onChange really isn't a side-effect here, it's just an effect of triggering the onChange event for <input>. So I do think your second version that calls both onChange and setValue together is more idiomatic.
If there were other ways of setting the value (e.g. a clear button), constantly having to remember to call onChange might be tedious, so I might write this as:
const MyCustomComponent = ({onChange}) => {
const [value, _setValue] = useState('');
// Always call onChange when we set the new value
const setValue = (newVal) => {
onChange(newVal);
_setValue(newVal);
}
return (
<input value={value} type='text' onChange={e => setValue(e.target.value)}></input>
<button onClick={() => setValue("")}>Clear</button>
)
}
But at this point this is hair-splitting.

The main purpose of the exhaustive-deps warning is to prevent the developers from missing dependencies inside their effect and lost some behavior.
Dan abramov – developer on Facebook core – strongly recommend to keep that rule enabled.
For the case of passing functions as dependencies, there is a dedicated chapter in the React FAQ:
https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies
tl;dr
If you have to put a function inside your dependencies array:
Put the function outside of your component, so you are sure that the reference won't be changed on each render.
If you can, call the function outside of your effect, and just use the result as dependency.
If the function must be declared in your component scope, you have to memoize the function reference by using the useCallback hook. The reference will be changed only if the dependencies of the callback function change.

Related

What's the best way to get input value in React

Today I was practicing with React and found two ways to get input values when submitting form.
First:
Using hooks, for example:
...
const [name, setName] = useState('');
...
return <input onChange={(value) => setName(value)} value={name} />
Second:
Get by using event.target.children[i].value, for example:
const handleSubmit = (event: BaseSyntheticEvent) => {
event.preventDefault();
const formInputs = event.target.children;
for (const input in formInputs) {
console.log(formInputs[input].value);
}
};
The question is, which one should I use and why? Is it optional, or I don't know, it does have some performance impact.
Don't use vanilla DOM manipulation in React when you can avoid it, within reason. The first approach is far better in most situations.
Working with the DOM is slow. State and the virtual DOM help a lot.
In the React paradigm, the appearance and functionality of the application should ideally stem from state as much as possible. Non-stateful side-effects can make things difficult to reason about and hard to work with given how re-rendering works and should be avoided.
If the reason you're tempted to go with the second approach is that you have a whole lot of inputs and making a separate state for each is tedious, one option is to make the state be a single object (or Map) instead.
const MyInput = ({ name, state, onChange }) => (
<Input name={name} onChange={onChange} value={state[name]} />
);
const onChange = (e) => {
setState({ ...state, [e.target.name]: e.target.value });
};
<MyInput name="name" state={state} onChange={onChange}/>

Is it ok to use useState hook setter to retrieve data, to avoid re-renders?

Short story
I want to know if this is ok to do:
const onClick = useCallback(() => {
setHoveredItem((hovered)=>{
setSelectedData(hovered);
return hovered;
});
},[]);
Just using the useState setter to get the value, 'removing' the useCallback's dependency to avoid re-renders. Notice the empty dependency array.
Is that an acceptable way to do things?
Long story
I am working on an application with an chart. Re-renders to the chart are really bad (they basically break). So I used React.memo. All good until I needed to pass event listeners:
<MemoChart
{...{ data, onHover, onClick }}>
</MemoChart>
On click, I want to store the hovered item.
Initially, this was my onClick function:
const onClick = () => {
setSelectedData(hoveredItem);
}
But of course, every hover event re-renders the parent, which causes the chart to re-render. Use Callback on onClick would work, except that if I add a dependency on hoveredItem, it will do nothing.
So this is what I did. It works, but I have never seen it done and I wonder if it is ok to do:
const onClick = useCallback(() => {
setHoveredItem((hovered)=>{
setSelectedData(hovered);
return hovered;
});
},[]);
UPDATED
For your case, I think you can use useRef to keep hovered data instead of a state.
const hoveredRef = React.useRef()
const onHover = useCallback((hovered) => {
hoveredRef.current = hovered //won't make re-rendering
}, [])
const onClick = useCallback(() => {
setSelectedData(hoveredRef.current)
}, [])
OLD ANSWER
<MemoChart
{...{ data, onHover, onClick }}>
</MemoChart>
Your problem is these events onHover and onClick will be initialized newly every time which will cause unexpected renderings. If you have a complex computation on your component, it will be laggy on renderings.
In your onClick case with useCallback, it works but it will create more trouble in state handlers because it's lacking dependencies which means it's considering that function will be the same all the time, but in fact, your function is relying on hovered state should be changed on events.
I'd propose that you should pass dependencies to align with your used states in functions.
const onHover = useCallback((hovered) => {
setHoveredItem(hovered);
}, []) //only initialize this function once after the component mounted
const onClick = useCallback(() => {
setSelectedData(hovered);
},[hovered]); //initialize this function again when `hovered` state changes
const onClick = useCallback(() => {
setHoveredItem((hovered)=>{
setSelectedData(hovered);
return hovered;
});
},[]);
This is not wrong, but it's a hack that helps you getting the fresh state if you have closures issues.
In your case there is no reason since you are not working with closures, and the problem is just that you set an empty deps array.
Now you say that this does not work:
const onClick = useCallback(() => {
setSelectedData(hovered);
},[hovered]);
But this MUST work, unless you are doing something wrong when you call setHovereditem.

Rewrite useEffect hook from componentDidUpdate lifecycle method

Suppose we have an input for buyer id and we want to fetch the buyer details each time the buyerId is changed.
The following code looks like this for class components
componentDidUpdate(prevProps,prevState) {
if (this.state.buyerId !== prevState.buyerId) {
this.props.fetchBuyerData(this.state.buyerId); // some random API to search for details of buyer
}
}
But if we want to use useEffect hook inside a functional component how would we control it. How can we compare the previous props with the new props.
If I write it as per my understanding it will be somewhat like this.
useEffect(() => {
props.fetchBuyerData(state.buyerId);
}, [state.buyerId]);
But then react's hooks linter suggests that I need to include props into the dependency array as well and if I include props in the dependency array, useEffect will be called as soon as props changes which is incorrect as per the requirement.
Can someone help me understand why props is required in dependency array if its only purpose is to make an API call.
Also is there any way by which I can control the previous state or props to do a deep comparison or maybe just control the function execution inside useEffect.
Deconstruct props either in the function declaration or inside the component. When fetchBuyerData is used inside the useEffect hook, then only it needs to be listed as a dependency instead of all of props:
// deconstruct in declaration
function MyComponent({ fetchBuyerData }) {
useEffect(() => {
// caveat: something is wrong with the use of `this.state` here
fetchBuyerData(this.state.buyerId);
}, [fetchBuyerData, state.buyerId]);
}
// deconstruct inside component
function MyComponent(props) {
const { fetchBuyerData } = props;
useEffect(() => {
// caveat: something is wrong with the use of `this.state` here
fetchBuyerData(this.state.buyerId);
}, [fetchBuyerData, state.buyerId]);
}
I'd assume you're rewriting your class component info functional one. Then you'd be better off including your fetch request right where you set new state.bayerId (I assume it's not an external prop). Something like:
const [buyerId, setBuyerId] = React.useState('')
const handleOnChange = (ev) => {
const { value } = ev.target
if (value !== buyerId) {
props.fetchBuyerData(value)
setBuyerId(value)
}
...
return (
<input onChange={handleOnChange} value={buyerId} />
...
The code snippet is somewhat suboptimal. For production I'd assume wrap change handler into useCallback for it to not be recreated on each render.

Using hooks from a nested componentDidMount()

I am trying to integrate hooks with some legacy React code. My old react code triggers a callback from a componentDidMount. I wanted to use a hook in the callback to update some state, but that doesnt seem to work: the hook gets stuck with the original value. Here's a pared down, reproducible example:
https://jsfiddle.net/0yf2xm96/16/
const Test = () => {
const [value, setValue] = React.useState(0);
return (
<div>
<Test2 onMount={() => setValue(value + 1)} />
<Test2 onMount={() => setValue(value + 1)} />
{ value /* displays 1, but I'd like to see 2 */ }
</div>
);
}
class Test2 extends React.Component {
componentDidMount() {
this.props.onMount();
}
render() { return null; }
}
ReactDOM.render(<Test />, document.querySelector("#app"))
I want that to display 2, not 1 - how would i go about doing that?
You can pass a function to the useState hook that takes the previous state value as an argument, as seen in the docs.
Change your onMount to:
<Test2 onMount={() => setValue(prev=>prev + 1)} />
<Test2 onMount={() => setValue(prev=>prev + 1)} />
Ciao, I suggest you to modify your code like this:
const Test = () => {
const [value, setValue] = React.useState(0);
return (
<div>
<Test2 onMount={() => setValue(value => value + 1)} />
<Test2 onMount={() => setValue(value => value + 1)} />
{ value }
</div>
);
}
Explanation: you know hook are async and if you call just setValue(value + 1) you are not considering the previous value of value. Bu tif you use arrow funcition the problem will be solved.
Here your code modified.
TLDR: Value is "stale". Use a functional update.
What's causing the problem
Lets think about this in plain javascript terms first.
Test is a function. Yes, its a component so it gets used like a special function with special JSX syntax, but ultimately its just a function.
With that understanding, we can also understand that value, while it is part of a useState hook, is just a javascript variable. In fact, its a const which means its value cannot be changed after it has been declared. I think this is a misunderstanding that most people have about the useState hook - just because it's stateful does not mean it can abandon the rules of being a const.
When you call a useState updater, it will signal React that the component needs to re-render. This re-render may not happen immediately. When the component is re-rendered, the function Test is called again. The implications of this are that a new const called value is created. The new const is assigned the value passed from the useState updater. This is what makes the hook so powerful. Setting state is not a simple variable assignment, it is a way to maintain values through multiple function calls.
Back to the issue:
When you declare two arrow functions like you did here:
<Test2 onMount={() => setValue(value + 1)} />
<Test2 onMount={() => setValue(value + 1)} />
You can hopefully now see that value in both of these functions is 0. Even after the first function is called, value is a const and still retains the value 0. Both functions get called before the next render cycle, which means the second one to be called is victim to value going stale.
Solution
Use the callback version of setting state referred to as functional updates. Most people eventually figure out the solution whether they understand why or not. When using the updater form, the value of state passed into the callback function is always guaranteed to use the most recent version, thereby removing the issue.

Correct way to define handlers in functional React components?

As far as I know, there are three ways to define functions in JavaScript.
1. Declaration
function handleEvent(e) {}
2. Assignment
var handleEvent = function(e) {}
3. Arrow
var handleEvent = (e) => {}
I have been searching for hours trying to find information on which of these options is the preferred way to declare handlers in functional React components. All the articles I found talk about class components, binding, etc. But with the new Hooks out, there must be standard for defining them inside the function as well. (After all, functional components have always existed anyway.)
Consider the following component, which declares three handlers, which are examples of different behaviour that you might need in a React component.
function NameForm(props) {
const [inputName, setInputName] = useState("");
useEffect(() => setInputName(props.initialValue), [props.initialValue]);
const handleInputChange = function(event) {
setInputName(event.target.value);
};
const resetForm = function() {
setInputName(props.initialValue);
props.onReset();
};
const handleFormSubmit = function(event) {
event.preventDefault();
props.onSubmit(inputName);
resetForm();
};
/* React-Bootstrap simple form example using these handlers. */
return (
<Form onSubmit={handleFormSubmit}>
<Form.Group controlId="NameForm">
<Form.Control
type="text"
placeholder="Enter your name here"
value={inputName}
onChange={handleInputChange}
/>
<Button type="submit">Submit</Button>
<Button onClick={resetForm}>Reset</Button>
</Form.Group>
</Form>
);
}
All of these handlers are directly passed as callbacks into other components. They might be called whenever, but at that exact moment, we need to have access to the current values of props and any state like inputName. Additionally, as you might have noticed, the handleFormSubmit handler also calls the resetForm handler.
What would be the recommended approach to defining the handlers from a performance perspective? Can it be avoidable that they are redefined on every render?
Does useCallback also fit in here somewhere?
The current standard is to declare the handlers as constants for immutability and as arrow-functions for binding purposes.
function NameForm(props) {
const [inputName, setInputName] = useState("");
useEffect(() => setInputName(props.initialValue), [props.initialValue]);
const handleInputChange = (event) => {
setInputName(event.target.value);
}
const resetForm = () => {
setInputName(props.initialValue);
props.onReset();
}
const handleFormSubmit = (event) => {
event.preventDefault();
props.onSubmit(inputName);
resetForm();
}
/* React-Bootstrap simple form example using these handlers. */
return (
<Form onSubmit={handleFormSubmit}>
<Form.Group controlId="NameForm">
<Form.Control
type="text"
placeholder="Enter your name here"
value={inputName}
onChange={handleInputChange}
/>
<Button type="submit">Submit</Button>
<Button onClick={resetForm}>Reset</Button>
</Form.Group>
</Form>
);
}
All of these handlers are directly passed as callbacks into other
components. They might be called whenever, but at that exact moment,
we need to have access to the current values of props and any state
like inputName
As currently constructed we meet the requirements for your description. Since props and state are defined as higher level data that the component has access to, all the event-handlers have access to them. And when used as a call-back in another component, they will remain bound to the initial component where they were defined in.
Which means if you have an event-handler like this:
const handleInputChange = (e) => {
setValue(e.target.value)
}
And you pass it into a ChildComponent like this:
<Child handleInputChange={handleInputChange}/>
And the Child uses the prop/event-handler like this:
<input onChange={props.handleInputChange}/>
You would be getting the event from the Child input, but you would be updating the state of the Parent, the original component that defined the event-handler.
There is virtually no difference between the "declaration", "assignment" and "arrow" approaches. The only thing that matters is that you don't always create new instances of the handler functions on each render. For this, use the useCallback hook to memoize the function references:
const handleInputChange = useCallback((event) => {
setInputName(event.target.value);
}, []); // `setInputName` is guaranteed to be unique, from the React Hooks docs, so no need to pass it as a dependency
const resetForm = useCallback(() => {
setInputName(props.initialValue);
props.onReset();
}, [props.onReset, props.initialValue]; // these come from props, so we don't know if they're unique => need to be passed as dependencies
const handleFormSubmit = useCallback((event) => {
event.preventDefault();
props.onSubmit(inputName);
resetForm();
}, [props.onSubmit, resetForm, inputName]); // `resetForm` and `inputName`, although defined locally, will change between renders, so we also need to pass them as dependencies
useCallback docs: https://reactjs.org/docs/hooks-reference.html#usecallback
In functional component we don't need access to this(even more - most linters will give you warning in such a case - and for a reason!). So it does not matter if we use arrow expression or declare a function.
But performance matters. Whatever option from your list you choose it will be recreated on each render. Declaring function itself is not really big deal but:
passed down to child it may cause unnecessary re-rendering for PureComponent/React.memo-wrapped components.
May cause redundant run for useMemo/other useCallback/useEffect if later one includes your handler into dependencies list.
So either declare handler out of the component(once it does not rely on internal state at all) or use useCallback. Beware it needs you explicitly list of all dependencies - don't ignore that. Otherwise result handler may operate on stale data.

Categories