In useEffect hook, how to find out what event triggered state change? - reactjs

I'm using useEffect hook to perform some sides effects after updating state. And that works perfectly fine, whenever child component invokes setOptions, useEffect fires as well.
I'm trying to figure out if there is a way to differentiate who updated options and based on that perform additonal work? More precisely, I would like to call another function in useEffect only if dependency was updated by specific component. Here's a dumbed down example of what I'm trying to do:
function App() {
const [value, setValue] = useState({});
const [options, setOptions] = useState([]);
useEffect(() => {
const newValues = await getNewValues();
setValue(newValues);
// If options have been changed from ChildComponentA
// Then doSomething() else nothing
}, [options]);
function doSomething() {}
return(
<>
<ChildComponentA handleChange={setOptions} />
<ChildComponentB handleChange={setOptions} />
</>
)
}
The example may be weird, it's just an example of the problem I'm having. The main problem is that state can be changed by multiple events, and I can't tell which event triggered the state change.

Maybe you could try adding an additional parameter to your options variable which would tell you from where the change was triggered, i.e options.child = 'A', would tell you it came from A and you could check that within the useEffect with a condition as if (options.child == 'A'), this of course would require options to be an object and not an array and that within the handleChange callback you do something like.
<ChildComponentA handleChange={(value) => {value.child ='A'; setOptions(value);}} />
Going deeper and as a personal recommendation, your app shouldn't aim to work different with the same state if it gets changed by one place or another since one of the main purposes of the state is to be a universal source of truth within your component and that each sub component that uses it treats it as such.

Related

Directly using several states included in useEffect's dependency array

Considering an useEffect with 2 different states in the dependency array. The useEffect hook will run whenever any of those two states are updated, but if i update one of them, will i have access to the lastest value of the other inside useEffect? And if not, what is the best approach to it?
function Component() {
const [state1, setState1] = useState('');
const [state2, setState2] = useState('');
useEffect(() => {
console.log(state1, state2)
}, [state1, state2]);
return <>...</>
}
The callback inside useEffect will run after the render conditionally based on dependency array.
If your state values are updated in the same render cycle then they are batched (by React) and the next render cycle will show both the correct values in the useEffect callback.
If you only update any one of them, you do not have to worry about the other value because the callback in useEffect will be using the recently updated value of the other variable too.
Note: The only time you might face an issue is when you have stale state values because of closure, but that is a specific case.

UseEffect triggering without respect to dependency array

I have a function below which i used as an array dependency to a useEffect handler
const handleInputUpdate = (event) => {
const eventValue = event.target.value;
setState({ ...state, answer_text: eventValue, trigger: true })
// console.log("I am changing for no reason")
}
Below is the useEffect handler
useEffect(() => console.log(" I am changing for no reason in useeffect"), [handleInputUpdate])
What i want is the useEffect handler to run only when the handleInputUpdate function is called but it runs also on component mount.
Here's what i've observed
The handleInputUpdate function doesn't run on component mount but only i need it to
Without respect to the above observation, the useEffect handler runs anyway.
Here's what i've tried
I tried consoling a text inside the handleInputUpdate function to see whether it runs on component render but it doesn't.
Even though the function doesn't run, the useEffect handler triggers anyway which is not what i want.
How can i solve this ?
Thanks in advance
useEffect dependency array is not used to trigger the effect when a function is called; the elements of the array are observed for any change and then trigger the effect.
In this case, handleInputUpdate will change on every render because it is not memoised, so the effect will also run on every render.
Since handleInputUpdate changes the state when it is called, you are better off adding that state to your useEffect dependency array:
useEffect(() => {
if (answer_text && trigger) {
console.log("I am changing for a reason in useeffect")
}
}, [answer_text, trigger])
The handleInputUpdate function, while it doesn't run on render, looks like it's created when the component runs, just before rendering. Since it won't be === to the value last in the dependency array - the handleInputUpdate from the prior render - the effect callback will run.
You need to observe changes to the answer_text value in state instead.
useEffect(() => {
// ...
}, [state.answer_text]);
I would also recommend separating out your state into different variables - these aren't class components, don't feel like you have to mash everything together into a single object structure.
const [text, setText] = useState('');

React Re-Render-Loop

I am currently trying to learn about the inner workings of React in context of when a component is re-rendered or especially when (callback-)functions are recreated.
In doing so, I have come across a phenomenon which I just cannot get my head around. It (only) happens when having a state comprising an array. Here is a minimal code that shows the "problem":
import { useEffect, useState } from "react";
export function Child({ value, onChange }) {
const [internalValue, setInternalValue] = useState(value);
// ... stuff interacting with internalValue
useEffect(() => {
onChange(internalValue);
}, [onChange, internalValue]);
return <div>{value}</div>;
}
export default function App() {
const [state, setState] = useState([9.0]);
return <Child value={state[0]} onChange={(v) => setState([v])} />;
}
The example comprises a Parent (App) Component with a state, being an array of a single number, which is given to the Child component. The Child may do some inner workings and set the internal state with setInternalValue, which in turn will trigger the effect. This effect will raise the onChange function, updating a value of the state array of the parent. (Note that this example is minimized to show the effect. The array would have multiple values, where for each a Child component is shown) However this example results in an endless re-rendering of the Child with the following console warning being raised throughout:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Debugging shows, that the re-rendering occurs due to onChange being changed. However, I do not understand this. Why is onChange being changed? Neither internalState nor state is changed anywhere.
There are two workarounds I found:
Remove onChange from the dependencies of the effect in the Child. This "solves" the re-rendering and would be absolutely acceptable for my use case. However, it is bad practice as far as I know, since onChange is used inside the effect. Also, ESLint is indicating this as a warning.
Using a "raw" number in the state, instead of an array. This will also get rid of the re-rendering. However this is only acceptable in this minimal example, as there is only one number used. For a dynamic count of numbers, this workaround is not viable.
useCallback is also not helping and just "bubbling up" the re-recreation of the onChange function.
So my question is: Do React state (comprising arrays) updates are being handled differently and is omitting a dependency valid here? What is the correct way to do this?
Why is onChange being changed?
On every render, you create a new anonymous function (v) => setState([v]).
Since React makes a shallow comparison with the previous props before rendering, it always results in a render, since in Javascript:
const y = () => {}
const x = () => {}
x !== y // always true
// In your case
const onChangeFromPreviousRender = (v) => setState([v])
const onChangeInCurrentRender = (v) => setState([v])
onChangeFromPreviousRender !== onChangeInCurrentRender
What is the correct way to do this?
There are two ways to correct it, since setState is guaranteed to be stable, you can just pass the setter and use your logic in the component itself:
// state[0] is primitive
// setState stable
<Child value={state[0]} onChange={setState} />
useEffect(() => {
// set as array
onChange([internalValue]);
}, [onChange, internalValue]);
Or, Memoizing the function will guarantee the same identity.
const onChange = useCallback(v => setState([v]), []);
Notice that we memoize the function only because of its nontrivial use case (beware of premature optimization).

Call Hooks on component change in React Native

I'm working on an react native app.
This app use a database, the main component use 2 differents hook.
The first hook retrieves the results of a SQL query and store them in a variable.
The second hook creates a list from the first variable
Like this:
const [people, setPeople ] = useState([]);
useEffect (() => {
db.getAllPeople().then(row => setPeople(row))
},[])
const [listData, setListData] = useState([]);
useEffect(()=> {
setListData(
Array(people.length)
.fill('')
.map((_, i) => ({ key: `${i}`, name: `${people[i].name}`}))
)
}, [people]);
After that, my main component displays a SwipeList from the results.
Here is the problem. I am using another component to add an element to my database. When I return to my main component I would like this new element to be displayed. But the problem is that the 2 hooks are not called on the component change and the list therefore remains unchanged.
I've tried to use the useFocusEffect but it doesn't work in my case.
Any suggestions ?
I think the useState hook manages the state of the component itself, unless you are passing this state among your parent and child or using callbacks to set the state on the component that you want to render, you could use a single source of truth to handle the changes in data, react itself will notice this changes and therefore, render the changed screens, considering that you have asynchronous operations when querying the database, a combination of redux and redux saga may help you.
https://github.com/redux-saga/redux-saga
There're one issues with your current code, or potential issues
Your second useEffect might get called when people becomes an empty list, this will reset your list data. The cure is to put a if statement inside, ex.
useEffect(()=> {
if (!people) return;
setListData(...)
}, [people]);
To be honest, if these two lists are connected, you shouldn't use two hook. The best way is to define listData
const listData = (a function that takes people as input), ex.
const listData = people.map(v => v)
Of course, there might be a reason why you'd like to introduce more hook in complex situation, ex. useRef, useMemo.

Is there any way to see names of 'fields' in React multiple state with React DevTools when using 'useState' hooks

I've been learning/experimenting with React hooks. When I go to inspect the values of the current state of a component using React DevTools in Chrome, I see the state fine, but the actual 'fields' -- that is, the state variables that are being updated by the individual useState hooks -- don't have any name associated with them. Instead, I see, for example, several strings, a couple of booleans, etc. I can generally figure out what's going on, but this seems problematic -- I'd like to be able to see which what the state variable's name is.
For instance, if I have something like
const [doughnuts, setDoughnuts] = useState(24)
When I look in React DevTools I'd like to see something like `doughnuts: number : 24', instead of just 'number: 24'.
Am I missing some setting somewhere, or some technique to turn on this ability?
Finally react team listened to us
The recent introduction of parsing custom hooks in react dev tools option might help
.
Before parsing ( before clicking the magic button in custom hooks card )
.
.
After parsing ( clicking the magic button in the top right )
Some approaches not mentioned in the other answers:
Use the following:(suggested by Oleh)
const [{ item }, setItem] = useState({ item: 2 });
You could also wrap the useState function so that, based on the shape of the initial value, the setItem function it returns auto-converts from the passed value itself into an object with the correct (wrapper) shape.
Create a new useStateWithLabel function:
function useStateWithLabel(initialValue, name) {
const [value, setValue] = useState(initialValue);
useDebugValue(`${name}: ${JSON.stringify(value)}`);
return [value, setValue];
}
It's based on the useDebugValue function described here.
Usage:
const [item, setItem] = useStateWithLabel(2, "item");
You are not missing anything and you can't change this behaviour. This is how React deals with multiple state.
https://reactjs.org/docs/hooks-rules.html#explanation.
One way to avoid this problem is to use a single State Hook which creates a single state including all the data.
const [state, setState] = useState({doughnuts: 24, key1: 'value1', key2: 'value2'});
In this case the state is stored in a single object and each value is associated with a key.
Take a look at this: Should I use one or many state variables?
A compound state is hard to manage, but there is a tool which can help you with that: useReducer Hook
When you do the following operation
const [item, setItem] = useSate(2)
You're using destructuring assignment in an array a type which does not contain a key like an object. You're just creating an alias to access the first element of the array returned by useState. If you do something like this
const [item, setItem] = useState({value: 2})
You will be able to see value: 2 in your dev-tools, cause it reflects the current state of that hook at a certain point of time.
Each time you call a Hook, it gets isolated local state within the currently executing component based on the previous value, so the identifier attributed by you (item) will only be scoped to that render cycle, but it doesn't mean that React reference is using the same identifier.
You can use useDebugState hook from use-named-state package.
import { useDebugState } from "use-named-state";
const App = () => {
const [counter, setCounter] = useDebugState("counter", 0);
return <button onClick={(prevCount) => prevCount + 1}>{counter}</button>;
};
It internally uses useDebugValue hook from react (method suggested by #Venryx)

Resources