React hooks - useEffect exhaustive deps - cyclic dependency on location.hash - javascript

I have a useEffect which reads the location.hash and based on some other dependencies, will change the hash. It looks something like this:
useEffect(() => {
const hashAlreadyPresent = () => {
const hashArr = history.location.hash.split('#');
return hashArr.includes(hashId);
};
const addToHash = () => {
return history.location.hash.concat(`#${hashId}`);
};
const removeFromHash = () => {
const hashArray = history.location.hash.split('#').filter(hashStr => hashStr);
const indexOfHashId = hashArray.indexOf(hashId);
(indexOfHashId !== -1) && hashArray.splice(indexOfHashId, 1);
return hashArray;
};
// if hashId props is present then attach hash in route
hashId && !hashAlreadyPresent() && history.push({
hash: `${hashAlreadyPresent() ? '' : addToHash()}`,
search: history.location.search,
});
return () => {
// remove hashId only, retain any other hash if present
const hashArray = removeFromHash();
hashId && hashAlreadyPresent() && history.replace({
hash: hashArray.join('#'),
search: history.location.search,
});
};
}, [history, hashId, history.location.hash, history.location.search]);
where history is from React Router.
The logic is that once the component is on screen (mounted), it adds a hash to the URL and once it is getting unmounted it will remove the hash from the url.
Of course, in terms of useEffect it translates to: if any of the dependencies change, the previous effect would be cleaned up and a new instance of the effect would be in place. The effective deps rule helped me with that, as earlier i was missing the fact that this hook should be cleaned up and re-run if hashId changes.
Now, we should have a dependency on history.location.hash for exhaustive deps, but the problem is every time I change hash from within the hook, the hook will run again (the previous instance will cleanup and change the hash again), which would lead to an infinite update kind of scenario.
NOTE: I know this is possible by switching off exhaustive-deps rule and excluding history.location.hash from dependencies, but would like to figure out any possibilities of refactoring/breaking down the useEffect, so that this can be solved without turning it off.
Another thing to note is that if I add history as a dependency (Which I have to because i am using a method from history), then the rule does not ask me to explicitly add the nested dependecies (history.lcoation.search, history.location.hash), but, those should be added, as the history object would remain the same but the nested objects would change on url change. This is same as the use case where you specify complete props object as a dependency instead of only the required specific nested properties.
Should i have a condition inside my useEffect based on when the location changes, which can somehow tell me if the location was changed from inside the hook and so don't do anything ?
Should i destructure and specify the dependencies in a different way, so that the effect does not run when the location.hash is changed from within the effect ?
NOTE:
had a discussion for this on github. got some more insights.
https://github.com/facebook/react/issues/19636

When a non-empty dependency array is specified, any value that you add to the dependency array causes the cleanup function to run first (except on the first render), followed by the effect function (except during unmount). To decide whether a value should go into the dependency array then, try answering this question for that value:
When this value is updated, should the effect run again, such that:
The desired effect is observed
Any potential changes made previously are cleaned up if required
No bugs due to stale references are introduced?
If the answer is yes for any of the points above, then that value makes it to the dependency array.
We can now answer the question above for all the values used within the useEffect function:
hashId: Yes. This is the primary driver for the effect, and each time this value changes, the URL should reflect the change. This becomes the source of truth for the effect. Therefore this is required to ensure that the desired effect is observed. Additionally, this is also required to clean up the previous hashId as the clean up function needs a reference to the previous hashId.
history: Yes. I suppose that as this is provided by react router, the reference should not change throughout the component's life cycle. In that sense, the only purpose of adding it here would be to satisfy the lint rule, with no real impact (other than an extra referential check). However, if it does change, the effect function will have a stale reference to it which could potentially cause bugs. This has to be taken care of.
history.location.search: No. This has nothing to do with the primary effect, as only the hashId is required to ensure that the desired effect is observed. There is also no danger of stale references, as this is always read from the history object. Since the history object is mutable and updated with the latest value every time, and is already a part of the dependency array, history.location.search can be safely omitted. *
history.location.hash: No, for the same arguments as for history.location.search. Additionally, it is always hashId that determines what the history.location.hash should be, so an update to this value should not be used to re-run the effect.
The final dependency array then is just [hashId, history]. **
* be careful to not extract out search from history.location and use search within the cleanup function, as it will be a stale reference
** noticed routeModal being used in the body of the effect, if needed this would also have to be a part of the dependency array

Related

Why should i create a copy of the old array before changing it using useState in react?

Why this works
const handleToggle = (id) => {
const newTodos = [...todos]
newTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
});
setTodos(newTodos);
}
And this doesnt
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
todo.completed = !todo.completed
}
}))
}
Why do i have to create a copy of the old todos array if i want to change some item inside it?
You are doing a copy of the array in both cases, and in both cases you are mutating the state directly which should be avoided. In the second version you also forgot to actually return the todo, so you will get an array of undefined.
Instead you should shallow copy the todo you want to update.
const handleToggle = (id) => {
setTodos(prevTodos => prevTodos.map(todo => {
if (todo.id === id) {
return {...todo, completed: !todo.completed}
}
return todo
}))
}
Why mutating state is not recommanded? source
Debugging: If you use console.log and don’t mutate state, your past
logs won’t get clobbered by the more recent state changes. So you can
clearly see how state has changed between renders.
Optimizations: Common React optimization strategies rely on skipping
work if previous props or state are the same as the next ones. If you
never mutate state, it is very fast to check whether there were any
changes. If prevObj === obj, you can be sure that nothing could have
changed inside of it.
New Features: The new React features we’re
building rely on state being treated like a snapshot. If you’re
mutating past versions of state, that may prevent you from using the
new features.
Requirement Changes: Some application features, like implementing
Undo/Redo, showing a history of changes, or letting the user reset a
form to earlier values, are easier to do when nothing is mutated. This
is because you can keep past copies of state in memory, and reuse them
when appropriate. If you start with a mutative approach, features like
this can be difficult to add later on.
Simpler Implementation: Because
React does not rely on mutation, it does not need to do anything
special with your objects. It does not need to hijack their
properties, always wrap them into Proxies, or do other work at
initialization as many “reactive” solutions do. This is also why React
lets you put any object into state—no matter how large—without
additional performance or correctness pitfalls.
In practice, you can often “get away” with mutating state in React,
but we strongly advise you not to do that so that you can use new
React features developed with this approach in mind. Future
contributors and perhaps even your future self will thank you!
when you're changing an object using a hook correctly (- in this case useState) you are causing a re-render of the component.
if you are changing the object directly through direct access to the value itself and not the setter function - the component will not re-render and it will likely cause a bug.
and the .map(function(...)=>{..}) is a supposed to return an array by the items that you are returning from the function within. since you're not returning anything in the second example - each item of the array will be undefined - hence you'll have an array of the same length and all the items within will be undefined.
these kinds of bugs will not happen if you remember how to use array functions and react hooks correctly,
it's usually really small things that will make you waste hours on end,
I'd really recommend reading the documentation.

Should you pass setter functions into the dependency array of a React hook?

Recently I saw a few examples of passing setter functions into hook dependency arrays in my coworkers' React code, and it doesn't look right to me. For example:
const MyComponent = () => {
const [loading, setLoading] = useState(true);
useEffect(() => {
doSomeBigLongNetworkRequest();
setLoading(false);
}, [setLoading, /* other deps */]);
// ...
}
My feeling is that they have misunderstood the purpose of the dependency array, which, as I understand it, is to indicate which pieces of state to monitor so that the hook can fire again when they change, not to simply indicate that the hook needs to use the setLoading function. And since the setLoading function never actually changes, including it in the dependencies does nothing.
Am I correct, or does including the setter in the array make sense somehow? My other thought was that maybe this was just a linter error, since the linter cannot recognize that the function is a setter, and thinks it might change.
I should also add that in the instances I've seen, they've included the setter but not the variable. So in the example above, setLoading, but not loading would be in the dependency array, and the hook does not actually need the value of loading.
Yes, you are right there is no need to include them. Here is quote from docs:
React guarantees that setState function identity is stable and won’t
change on re-renders. This is why it’s safe to omit from the useEffect
or useCallback dependency list.
In general again based on docs the recommendation about dependency array is:
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.

How can I use an object as initializer for custom hooks without adding complexity/state or inviting future problems?

I just started using hooks in react and am creating a prototype custom hook for a framework.
The hook should take an object as an argument for initialization and cleanup (setting up/removing callbacks for example).
Here is my simplified Code so far:
export function useManager(InitObj) {
const [manager] = useState(() => new Manager());
useEffect(() => {
manager.addRefs(InitObj)
return () => manager.removeRefs(InitObj)
}, [manager]);
return manager;
}
to be used like this:
useManager({ cb1: setData1, cb2: setData2... })
In future Iterations the Manager might be a shared instance, so I need to be able to be specific about what I remove upon cleanup.
I put console.log all over the place to see If i correctly understand which code will be run during a render call. From what I can tell this code does 100% what I expeted it to do!
Unfortunately (and understandably) I get a warning because I did not include InitObj in the effects dependencies. But since I get an object literal simply putting it in there will cause the effect to be cleaned up/rerun on every render call since {} != {} which would be completely unnecessary.
My research so far only revealed blog posts like this one, but here only primitive data is used that is easily classified as "the same" (1 == 1)
So far I have found 3 possible solutions that I am not completely happy with:
using useMemo to memoize the object literal outside the hook
useManager(useMemo(() => { cb: setData }, []))
This adds more responsibility on the developer using my code => not desirable!
using useState inside the hook
const [iniOBj] = useState(InitObj);
A lot better already, but it adds state that does not feel like state. And it costs (minimal) execution time and memory, I would like to avoid that if possible.
using // eslint-disable-next-line react-hooks/exhaustive-deps
Works for sure, but there might still be other dependencies that might be missed if I simply deactivate the warning.
So my question is:
How can I use an object as initializer for custom hooks without adding complexity/state or inviting future problems?
I half expect that the useState option will be my best choice, but since I am new to hooks there might still be something that eluded my understanding so far.

Should refs be in listed as dependencies for useEffect and such?

As I understand, the containers returned by useRef are always the same - yet referencing them in useEffect and similar functions results in eslint exhaustive-deps warning. Is it safe to ignore the warning in this case, and what's a good way to avoid both clogging the output log with warnings, and my code with disable-line comments? Or should I just stick them into dependency list to keep eslint happy?
When useRef is first called it creates an object with a current property. This object will remain the same across subsequent renders. Ie: the reference to this object won't change.
https://reactjs.org/docs/hooks-reference.html#useref
So it's safe to omit it from the dependencies array.
See the code below (also available in the Sandbox link):
https://codesandbox.io/s/cocky-dhawan-ys267?file=/src/App.js
const someRef = useRef({foo: "bar"});
let x = 1;
useEffect(() => {
console.log(someRef.current.foo);
console.log(x);
}, []); // THERE IS A WARNING HERE FOR THE "x"
eslint/exhaustive-deps is only worrying about the x, and not the someRef.current.foo.
NOTE: I've just put the x there to make sure the warnings were being triggered by eslint.
The reason behind this is that useRef isnot related to the render cycle. I mean, it's not recreated, neither is automatically updated after every render, like state, props or variables created during render usually are.
Are you sure you are getting this warning for useRef? See the CodeSandbox link and give it a double check. Check how are you referencing them into the useEffect and also check your React and Eslint/plugin versions.

Calling setState without triggering re-render

I am storing a UI state in the React component's state, say this.state.receivedElements which is an array. I want re-renders whenever an element is pushed to receivedElements. My question is, can I not trigger rendering when the array becomes empty ?
Or in general, can I call setState() just one time without re-render while re-rendering all other times ? ( are there any options, work-arounds ? )
I've read through this thread: https://github.com/facebook/react/issues/8598 but didn't find anything.
I want re-renders whenever an element is pushed to receivedElements.
Note that you won't get a re-render if you use:
this.state.receivedElements.push(newElement); // WRONG
That violates the restriction that you must not directly modify state. You'd need:
this.setState(function(state) {
return {receivedElements: state.receivedElements.concat([newElement])};
});
(It needs to be the callback version because it relies on the current state to set the new state.)
My question is, can I not trigger rendering when the array becomes empty ?
Yes — by not calling setState in that case.
It sounds as though receivedElements shouldn't be part of your state, but instead information you manage separately and reflect in state as appropriate. For instance, you might have receivedElements on the component itself, and displayedElements on state. Then:
this.receivedElements.push(newElement);
this.setState({displayedElements: this.receivedElements.slice()});
...and
// (...some operation that removes from `receivedElements`...), then:
if (this.receivedElements.length) {
this.setState({displayedElements: this.receivedElements.slice()});
}
Note how we don't call setState if this.receivedElements is empty.
What about useRef?
Documentation says:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
So if you change ref value inside useEffect it won’t rerender component.
const someValue = useRef(0)
useEffect(() => {
someValue.current++
},[])

Categories

Resources