I trying to wrap my head around the new hooks api of react. Specifically, I'm trying to construct the classic use case that once was the following:
componentDidUpdate(prevProps) {
if (prevProps.foo !== this.props.foo) {
// animate dom elements here...
this.animateSomething(this.ref, this.props.onAnimationComplete);
}
}
Now, I tried to build the same with a function component and useEffect, but can't figure out how to do it. This is what I tried:
useEffect(() => {
animateSomething(ref, props.onAnimationComplete);
}, [props.foo]);
This way, the effect is only called when props.foo changes. And that does work – BUT! It appears to be an anti-pattern since the eslint-plugin-react-hooks marks this as an error. All dependencies that are used inside the effect should be declared in the dependencies array. So that means I would have to do the following:
useEffect(() => {
animateSomething(ref, props.onAnimationComplete);
}, [props.foo, ref, props.onAnimationComplete]);
That does not lead to the linting error BUT it totally defeats the purpose of only calling the effect when props.foo changes. I don't WANT it to be called when the other props or the ref change.
Now, I read something about using useCallback to wrap this. I tried it but didn't get any further.
Can somebody help?
I would recommend writing this as follows:
const previousFooRef = useRef(props.foo);
useEffect(() => {
if (previousFooRef.current !== props.foo) {
animateSomething(ref, props.onAnimationComplete);
previousFooRef.current = props.foo;
}
}, [props.foo, props.onAnimationComplete]);
You can't avoid the complexity of having a condition inside the effect, because without it you will run your animation on mount rather than just when props.foo changes. The condition also allows you to avoid animating when things other than props.foo change.
By including props.onAnimationComplete in the dependencies array, you avoid disabling the lint rule which helps ensure that you don’t introduce future bugs related to missing dependencies.
Here's a working example:
Suppress the linter because it gives you a bad advice. React requires you to pass to the second argument the values which (and only which) changes must trigger an effect fire.
useEffect(() => {
animateSomething(ref, props.onAnimationComplete);
}, [props.foo]); // eslint-disable-line react-hooks/exhaustive-deps
It leads to the same result as the Ryan's solution.
I see no problems with violating this linter rule. In contrast to useCallback and useMemo, it won't lead to errors in common case. The content of the second argument is a high level logic.
You may even want to call an effect when an extraneous value changes:
useEffect(() => {
alert(`Hi ${props.name}, your score is changed`);
}, [props.score]);
Move the values, that must be fresh (not stale) in the callback but mustn't refire the effect, to refs:
const elementRef = useRef(); // Ex `ref` from the question
const animationCompleteRef = useRef();
animationCompleteRef.current = props.onAnimationComplete;
useEffect(() => {
animateSomething(elementRef, animationCompleteRef.current);
}, [props.foo, elementRef, animationCompleteRef]);
It works because useRef return value doesn't change on renders.
Related
This is my component:
const FooComponent = () => {
const initFoo = {
attrOne: '',
attrTwo: '',
attrThree: '',
};
const fooContext = useContext(FooContext);
const { addFoo, hasFoo, currentFoo } = fooContext;
const [foo, setFoo] = useState(initFoo);
useEffect(() => {
if (hasFoo) {
setFoo(currentFoo);
} else {
setFoo(initFoo);
}
}, [fooContext, currentFoo]);
...
....
}
I'm seeing the following warning in the console
Line 21:8: React Hook useEffect has a missing dependency: 'initFoo'. Either include it or remove the dependency array react-hooks/exhaustive-deps
Adding initFoo in the array [fooContext, currentFoo] that is passed in useEffect gives me further warnings and doesn't seem to fix the previous one. Both fooContext and currentFoo are required as inputs in the array as the functionality in the useEffect hook needs to be triggered whenever one of these two change a value.
Line 5:11: The 'initFoo' object makes the dependencies of useEffect Hook (at line 21) change on every render. To fix this, wrap the initialization of 'initFoo' in its own useMemo() Hook react-hooks/exhaustive-deps
Any ideas how I can fix this? I've tried to search for similar cases this warning pops, but haven't found a similar example in here
In your component definition, every time it re-renders, you're re-creating the variable initFoo.
React hook linting isn't smart enough to figure out this case. It says "hey, I see you're creating a new object every time your component renders. However, you don't update your useEffect callback, which only is set once. So if your initFoo changes without your useEffect changing, then your useEffect could end up setting an old value of initFoo, causing bugs."
In your case, initFoo is the "same" every time. Moving initFoo outside of the component definition stops it from being re-created on every component render, so React hook linting sees that it can never change, so the useEffect callback could never become "stale" based on the value of initFoo changing.
Another option is to inline the use of initFoo:
useState({ attrOne: '', ... });
Which may be inconvenient here if you need to duplicate the result in useEffect.
This is also a warning, not an error, which you can ignore if you prefer to keep the declaration inside the component body for some reason. You can disable any ESLint rule with comments, if you want to ignore this warning, put this line above your useEffect call:
// eslint-disable-next-line react-hooks/exhaustive-deps
I am passing state as a variable down to a component via props like to...
const [someState, setSomeState] = useState([])
PlaceDataInIndexDB({ setSomeState: setSomeState,
user: props.user })
And in the PlaceDataInIndexDB.js I have a useEffect which eventually sets the state using
useEffect(() => {
props.setSomeState([array])
}), [props.user]
The issue is that I get a warning saying I need to use props in my dependency array, however, I do not want the useEffect to run every time props change because I have a lot of other props in there. What can I do for this? This is an extremely simplified version of my code. i can post exact code but it is a lot more to look through.
And here is the warning...
React Hook useEffect has a missing dependency: 'props'. Either include
it or remove the dependency array. However, 'props' will change when
any prop changes, so the preferred fix is to destructure the 'props' object outside of the useEffect call and refer to those specific props
inside useEffect react-hooks/exhaustive-deps
It's telling you what the issue is. Generally, anything referenced inside of the useEffect function needs to also exist in the dependency array, so React knows to run it again when those things change. If the thing you're using is a property of some other object (like props), it's best to pull the values out of there prior to referencing them.
const PlaceDataInIndexDB = (props) => {
const { setSomeState, user } = props
useEffect(() => {
setSomeState([array])
}), [setSomeState, array] // <-- your example doesn't show where `array` comes from, but needed here as well
// ...
}
You can, in fact, destructure the props inline so that you never even have a reference to the entire props object:
const PlaceDataInIndexDB = ({ setSomeState, user }) => {
Note that setSomeState must also be in the dependency array. If -- and only if -- the useState is in the same component, the linter is smart enough to know that it never changes and lets you leave it out. However, if it's passed in as a prop from somewhere else, there is no way for it to know.
The linting rules for hooks are very good. By and large, unless you really know what you're doing, you can blindly follow the suggestions it makes. Eslint actually added an entire "suggestions" feature for this specific rule, which e.g. vscode will change for you if you put your cursor on the error and press ctrl+.
There are articles related to what is useCallback and to use it when necessary. But in a big project it's very difficult to understand when to start using this.
So, if I use it for every use case, will it hit the performance of the application.
useCallback and useMemo are helper hooks with the main purpose off adding an extra layer of dependency check to ensure synchronicity. Usually you want to work with useCallback to ensure a stable signature to a prop which you know how will change and React doesn't.
A function(reference type) passed via props for example
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [])
}
Lets assume you have a child component which uppon mounting must set some state in the parent (not usual), the above code will generate a warning of undeclared dependency in useEffect, so let's declare setParentState as a dependency to be checked by React
const Component = ({ setParentState }) =>{
useEffect(() => setParentState('mounted'), [setParentState])
}
Now this effect runs on each render, not only on mounting, but on each update. This happens because setParentState is a function which is recreated every time the function Component gets called. You know that setParentState won't change it's signature overtime so it's safe to tell React that. By wrapping the original helper inside an useCallback you're doing exactly that (by adding another dependency check layer).
const Component = ({ setParentState }) =>{
const stableSetter = useCallback(() => setParentState(), [])
useEffect(() => setParentState('mounted'), [stableSetter])
}
There you go. Now React knows that stableSetter won't change it's signature inside the lifecycle therefore the effect do not need run unecessarily.
On a side note useCallback it's also used like useMemo, to optmize expensive function calls (memoization).
The two main purposes of useCallback are
Optimize child components that rely on reference equality to prevent unnecessary
renders. Font
Memoize expensive computing calculations
Is there any drawback/performance?
useCallback is mainly used to optmize performance by changing stuff only when it's needed. It does that by adding a layer of dependencies just like useEffect (and similar to how React itself knows how something must change in the DOM), but as every single performance optimization technique, it can shoot backwards, if you fill your entire application with unecessary useCallback, useMemo, React.memo the performance will go the other way around.
So the key is to use wisely, carefully choosing what needs to have a stable signature and what doesn't.
I've never used hooks in React and I'm trying to use useEffect() but I don't seem to get the basics of its correct structure and use.
I was able to achieve the results with plain JavaScript but with useState the state remains untouched.
Then I found useEffect after searching for a while, and this is what I could get it to look like-
// Background Parallax Effect
let [translateY,setTranslateY] = useState(0);
useEffect(()=> {
const getScrollPos = ()=> {
setTranslateY(window.scrollY*(-.2));
requestAnimationFrame(getScrollPos);
}
document.addEventListener("DOMContentLoaded",getScrollPos)
},[translateY]);
I highly suspect that its structure isn't as it is supposed to be.
So I want to know the fixes and how it exactly works to help understand the structure better.
The issue with your first code is that you add translateY as a dependency to useEffect. . You should remove translateY as a dependency and also remove the event listener when the component unmounts. Also you have a requestAnimationCallback within the getScrollPos function which is triggered unconditionally causing infinite loop
useEffect(()=> {
const getScrollPos = ()=> {
setTranslateY(window.scrollY*(-.2));
}
const setScrollPos = () => {
requestAnimationFrame(getScrollPos);
}
document.addEventListener("DOMContentLoaded",setScrollPos);
return () => {
document.removeEventListener("DOMContentLoaded",setScrollPos)
}
},[]);
Note that if you update the state with same value, react prevents a re-render.
In the second code, although you call the state update by using listenScroll directly in render function, it doesn't cause a loop because you would be setting the same value to update state and hence an infinite loop doesn't occur
I am trying to update context once a Gatsby page loads.
The way I did it, the context is provided to all pages, and once the page loads the context is updated (done with useEffect to ensure it only happens when the component mounts).
Unfortunately, this causes an infinite render loop (perhaps not in Firefox, but at least in Chrome).
Why does this happen? I mean, the context update means all the components below the provider are re-rendered, but the useEffect should only run once, and thats when the component mounts.
Here is the code: https://codesandbox.io/s/6l3337447n
The infinite loop happens when you go to page two (link at bottom of page one).
What is the solution here, if I want to update the context whenever a page loads?
The correct answer for this issue is not to pass an empty dependency array to useEffect but to wrap your context's mergeData in a useCallback hook. I'm unable to edit your code but you may also need to add dependencies to your useCallback like in my example below
import React, { useState, useCallback } from "react"
const defaultContextValue = {
data: {
// set initial data shape here
menuOpen: false,
},
mergeData: () => {},
}
const Context = React.createContext(defaultContextValue)
const { Provider } = Context
function ContextProviderComponent({ children }) {
const [data, setData] = useState({
...defaultContextValue,
mergeData, // shorthand method name
})
const mergeData = useCallback((newData) {
setData(oldData => ({
...oldData,
data: {
...oldData.data,
...newData,
},
}))
}, [setData])
return <Provider value={data}>{children}</Provider>
}
export { Context as default, ContextProviderComponent }
The selected answer is incorrect because the react docs explicitly say not to omit dependencies that are used within the effect which the current selected answer is suggesting.
If you use es-lint with the eslint-plugin-react-hooks it will tell you this is incorrect.
Note
If you use this optimization, make sure the array includes all values
from the component scope (such as props and state) that change over
time and that are used by the effect. Otherwise, your code will
reference stale values from previous renders. Learn more about how to
deal with functions and what to do when the array changes too often.
https://reactjs.org/docs/hooks-effect.html
Is it safe to omit functions from the list of dependencies? Generally
speaking, no. It’s difficult to remember which props or state are used
by functions outside of the effect. This is why usually you’ll want to
declare functions needed by an effect inside of it. Then it’s easy to
see what values from the component scope that effect depends on:
https://reactjs.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies
By default, useEffect runs every render. In your example, useEffect updates the context every render, thus trigger an infinite loop.
There's this bit in the React doc:
If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works.
So applies to your example:
useEffect(() => {
console.log("CONTEXT DATA WHEN PAGE 2 LOADS:", data)
mergeData({
location,
})
- }, [location, mergeData, data])
+ }, [])
This way, useEffect only runs on first mount. I think you can also leave location in there, it will also prevent the infinite loop since useEffect doesn't depend on the value from context.