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
Related
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+.
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 have component that uses both useState() and other custom hooks multiple times.
I want to act based on these state values. I could do that directly in the function component's body:
const MyComponent = () => {
const [someState, setSomeState] = useState(false);
const [otherState, setOtherState] = useState(false);
const customHookValue = useCustomHook();
if (someState) foo();
const foo = () => setOtherState(!otherState);
if (customHookValue > 10) bar();
const bar = () => setSomeState(somestate > customHookValue);
}
However, every time someState changes (and a re-render happens) the second conditional also runs, causing bar() to run if the conditional passes. This feels unnatural. Logically, bar() should only run when customHookValue changes, as someState only changes if customHookValue has changed since last render.
In summary, a re-render caused by a change to someState should not cause a bunch of unrelated state setting functions to run again. Even though re-running them causes no change in the outcome of the program, it is not logically right. They only need to re-run when their corresponding conditional changes. It could effect performance.
This must be a common challenge in React. I am quite new to it, so I do not know what the approach to solve this would be.
Questions
How would I solve the above in the recommended manner?
Would I have to wrap every conditional in a useEffect or a useMemo?
EDIT:
Updated the second conditional, to make my question clearer (it should depend on customHook).
CLARIFICATION:
As it might not have been clear, my issue is as follows. When state changes, a re-render occurs. This causes all functions in the component's body to re-run. Now, if we have many useState in a component, and only one changes, a bunch of potentially unrelated state-changing and potentially expensive functions I have defined in the components body will run. These state-changing functions would only have to run if the state values they are trying to set has changed. If the values they are setting has not changed, it is unnecessary for them to run again. But, the re-render reruns all functions in the component's body regardless.
It looks like (as others have suggested) you want useEffect:
useEffect(() => {
if (someState) {
setOtherState(!otherState)
}
}, [someState, otherState])
useEffect(() => {
if (customHookValue > 10) {
setSomeState(someState > customHookValue)
}
}, [customHookValue])
Since you only want the setSomeState to run if customHookValue changes, make it the only item in the dependencies array passed to useEffect.
The exhaustive-deps eslint-plugin-react-hooks will complain about the second useEffect, since the function depends on the value of someState, even though someState will only potentially change if customHookValue changes. I also wouldn't worry about things potentially affecting performance until they do. I don't know a ton about the internals of React, but it does some things under the hood to avoid re-renders it doesn't need to do, and can do multiple renders before an actual update is painted.
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.
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.