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
Related
I often encounter the situation that I build a window.requestAnimationFrame based render loop, but want some states to be exchanged with the inside of the loop. There are a few ways to accommplish that, but I'm not sure which on is the best. My setup usually looks something like this:
function Animation({someState}) {
const loopDataRef = useRef();
useEffect(() => {
//do something initialization work for the renderloop, e.g. getContext, etc.
let frame;
const loop = (cts) => {
frame = window.requestAnimationFrame(loop)
//do some loop work using someState and loopDataRef.current
}
loop();
return () => window.cancelAnimationFrame(frame);
}, []);
}
Note that using someState inside the loop will always use the value from the last ran of the effect, not necessarily the current value. Here are the some obvious solutions:
I can put someState in the dependency array of useEffect, this way the loop will be restarted whenever the state changes. But if the initialization is expensive, for example with WebGL where I create all the textures there and compile the shaders, it doesn't seem very elegant.
I can use two effects, one for the initialization of the loop itself, and another one for just the loop, which stops and starts on every state change. But I still think, ideally an animation loop should run until it stops and not stop/start in between.
Another solution with two effects, but this time I do const someStateRef = useRef() and then create an effect with someState in its dependency array which just writes someState into someStateRef.current so I can use it inside the loop. Here is an example where I implement this. Alternatively one can write someState into someStateRef.current on every render, without another effect. Looks very performant, but not really elegant.
What's the most React way to do it?
I'd go with the 3rd option that you already implemented, as it looks exactly like one example from the React Hooks FAQ (3rd code block):
function Example(props) {
// Keep latest props in a ref.
const latestProps = useRef(props);
useEffect(() => {
latestProps.current = props;
});
useEffect(() => {
function tick() {
// Read latest props at any time
console.log(latestProps.current);
}
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, []); // This effect never re-runs
}
(replacing set/clearInterval with request/cancelAnimationFrame)
For completeness: The first two options suffer from the flaws you mentioned, and writing someState to someStateRef.current on every render without another effect is not recommended.
Avoid reading and updating refs during rendering because this makes your component’s behavior difficult to predict and understand.
source
I've got the following component (simplified) which, given a note ID, would load and display it. It would load the note in useEffect and, when a different note is loaded or when the component gets unmounted, it saves the note.
const NoteViewer = (props) => {
const [note, setNote] = useState({ title: '', hasChanged: false });
useEffect(() => {
const note = loadNote(props.noteId);
setNote(note);
return () => {
if (note.hasChanged) saveNote(note); // bug!!
}
}, [props.noteId]);
const onNoteChange = (event) => {
setNote({ ...note, title: event.target.value, hasChanged: true });
}
return (
<input value={note.title} onChange={onNoteChange}/>
);
}
The issue is that within the useEffect I use note, which is not part of the dependencies so it means I always get stale data.
However, if I put the note in the dependencies then the loading and saving code will be executed whenever the note is modified, which is not what I need.
So I'm wondering how can I access the current note, without making it a dependency? I've tried to replace the note with a ref, but it means the component no longer updates when the note is changed, and I'd rather not use references.
Any idea what would be the best way to achieve this? Maybe some special React Hooks pattern?
You can't get the current state because this component does not render on the app render that removes it. Which means your effect never runs that last time.
Using an effect cleanup function is not a good place for this sort of thing. That should really be reserved for cleaning up that effect and nothing else.
Instead, whatever logic you have in the app that changes the state to close the NoteViewer should also save the note. So in some parent component (perhaps a NoteList or something) you'd save and close like:
function NoteList() {
const [viewingNoteId, setViewingNoteId] = useState(null)
// other stuff...
function closeNote() {
if (note.hasChanged) saveNote(note)
setViewingNoteId(null)
}
return <>{/* ... */}</>
}
React docs state: don’t call Hooks inside loops, conditions, or nested functions.
Does calling a hook means just calling useState e.g. const [state, useState] = useState(0)?
What about calling setter in conditionals ?
Is this code breaking rules of hooks ?
const [oneHook, setOneHook] = useState(0)
const [anotherHook, setAnotherHook] = useState(false)
if (something) {
setOneHook(1)
setAnotherHook(true)
} else {
setOneHook(0);
setAnotherHook(false)
}
Thanks !
No, that code example does not break the rules of hooks. Every time the component renders there will be exactly the same number of calls to useState in exactly the same order, so that will be fine.
I do want to point out that setting state right away in the body of the component doesn't make much sense. When the component mounts it will start rendering with the initial values from state, but then before it can finish rendering the state has already changed and it has to start over. But presumably that's just an artifact of the example, and in your real code the if would be inside a useEffect or some other practical code location.
React docs state: don’t call Hooks inside loops, conditions, or nested functions.
Alright,the following code shows the example for the above statement. I had the same issue where i needed to set the state inside of a loop, like following
const [someArray, setArray] = useState([]);
someValue.forEach((value, index) => {
setArray([...someArray, value]) //this should be avoided
});
Above thing i have achieved like this following
var temp = [];
var counter = 0;
someValue.forEach((value, index) => {
temp.push(value);
counter++;
if (counter === someValue.length) {
setArray(temp)
}
});
if you are setting a state inside the loop than each time the component re renders which you do not want to get into.
Is this code breaking rules of hooks
No Your code looks fine, as you are setting up the state only based on condition for only once when the component renders
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.