useEffect not triggering - reactjs

I think my useEffect is broken.
I have on the parent component
<div className={redactKategori}>
<select className="form-control form-control-sm mb-3 mr-2" name="betalt" defaultValue={'välj kategori'} onChange={e => { e.preventDefault(); setKate(e.target.value); setRedactKategoris('d-block') }}>
<option >valj kategori</option>
{ Kategori ?
Object.keys(Kategori).map(key => {
return (
<option key={key} value={key}>{key}</option>
)
}) :
null
}
</select>
<div className={redactKategoris}>
<EditKat aa={kate} />
</div>
and on the child component
function EditKat({ aa }) {
let defaultValues = 0
useEffect(() => {
defaultValues = 2
}, [aa])
console.log(defaultValues)
}
So, as far as I understood, everytime ${kate} would get a value on the parent component, the child component would trigger the useEffect hook. However it is not working. Am I doing something wrong?

The reason for the experienced behavior is not that useEffect isn't working. It's because of the way function components work.
If you look at your child component, if useEffect is executed and the component rerenders, defaultValues would be set to 0 again, because the code inside of the function is executed on each render cycle.
To work around that, you would need to use the useState to keep your local state consistent across renders.
This would look something like this:
function EditKat({ aa }) {
// Data stored in useState is kept across render cycles
let [defaultValues, setDefaultValues] = useState(0)
useEffect(() => {
setDefaultValues(2) // setDefaultValues will trigger a re-render
}, [aa])
console.log(defaultValues)
}

Try adding a key prop on your component when it is created in the parent code
<yourcomponent key="uniquevalue" />
This is because in most cases, when your component is reused, based on the way it is created, it may usually re-render it with some changes instead of recreating it again when you reuse it, Hence the useEffect is not going to be called. eg in SwitchRoute, loops, conditionals...
So adding a key will prevent this from happening. If it is in a loop make sure each element is unique, maybe by including the index i in the key if you can't find any better unique key.

I faced the same problem, I debugged it and i found that, i am mutating the state directly instead of cloning it and using it. So, that's why useEffect is not triggered.

Related

React preserving state even though key has changed

The react component is preserving the "wasEdited" state even when the properties and key are changed. When any div element is edited from its initial value, the classname of the div row will be changed, thus showing a red border on the left side.
I was able to get around the issue by resetting the state manually every time the "globalId" property is changed. As you can see in the comment, I suspect this may be a similar issue to this:
React does not re-render updated map, even with different keys
But in my case I'm not mutating an object.
function Attribute ({globalId, name, value}) {
const [wasEdited, setWasEdited] = useState(false);
const handleInput = (e) => {
e.target.innerHTML === value ? setWasEdited(false) : setWasEdited(true);
};
// TODO: This useEffect is a hack to reset the wasEdited state when the globalId changes. I don't understand
// why this is necessary, since each Attribute component is rendered with a unique key. I think it
// has something to do with this post on [stack overflow](https://stackoverflow.com/questions/68949376///react-does-not-re-render-updated-map-even-with-different-keys).
// useEffect(() => {
// return () => setWasEdited(false);
// }, [globalId])
console.log(`${globalId}-${name}`, wasEdited)
return (
<div key={`${globalId}-${name}`} className={wasEdited ? 'row was-edited' : 'row'}>
<div className='col'>{name}</div>
<div className='col' onInput={handleInput} contentEditable>{value}</div>
</div>
)
}
This is what it looks like:
Putting a key on a div will cause that div to unmount and remount when the key changes. But it will have no effect on the component surrounding that div (Attribute). If you want to deliberately unmount and remount Attribute, you need to put a key on Attribute. So wherever you're rendering it, do:
<Attribute key={`${globalId}-${name}`} globalId={globalId} name={name} value={something} />

React: Hook not triggering when parent component changes boolean value passed to child

so I have a parent that is responsible for rendering a list of items. When the item is clicked it will send an event to the parent, that will record on a Map the selection state. If selected it passes the selected state to the child so that it can change style. Here's the code for the parent:
export default function CreateYourMixProductsList(props: ProductsInterface) {
const selectedProducts: Map<number, Product> = new Map<number, Product>();
function onProductClick(product: Product): void {
selectedProducts.set(product.id, product);
}
return (
<>
<List sx={{width: '100%', bgcolor: 'background.paper'}}>
{props?.products?.map(product => (
<ProductListItem key={product.id} product={product}
selected={selectedProducts.has(product.id)}
onClick={(product) => onProductClick(product)} />
))}
</List>
</>
);
and the child
export default function ProductListItem(props: ProductProps) {
const [selected, setSelected] = React.useState(false);
function onClick(product: Product) {
props.onClick(product);
}
useEffect(() => {
setSelected(!selected);
}, [props.selected]);
return (
<>
<ListItemButton alignItems="flex-start" onClick={event => {onClick(props.product)}} selected={props.selected ? selected : false}>
//omitted code to keep it short
The useEffect is triggered only on rendering, whilst to my understanding, it should be triggered every time the props passed down is an immutable variable. What am I missing here?
here's a bit of documentation on useEffect
https://reactjs.org/docs/hooks-effect.html
By default, useEffect will trigger after each render. Adding an array will skip this process and instead only trigger the useEffect on the changing of the specified prop(s). If we take a look at your code example:
useEffect(() => {
setSelected(!selected);
}, [props.selected]);
this block is triggering the useEffect each time props.selected is updated, hence why it only triggers on render, when you give the value of false to selected. What you can do instead is call setSelected(!selected) as part of the onClick event. Once this is occurs, you can update the useEffect to handle any logic that should occur when selected changes, or remove the useEffect entirely.
Lets unwrap this:
Why is the useEffect not running?
Hooks re-run every time a variable in their dependency array changes. Your hook is not running again because the value of props.selected does not change.
You can easily verify this by simply logging the value in the component.
Why is props.selected not changing?
Your click handler correctly sets the value on your Map. However, React does not recognize that a a new value was set inside the map. The component never re-renders and selectedProducts.has() is not called again. So the value of props.selected is indeed still the same.
How can you make React recognize your change?
First of all, you should avoid declaring state like this in your component. Each time this component renders it will re-declare all variables defined inside it (selectedProducts will always be set to a new empty map). Use reacts hook api instead.
To make the variable stick - and reactive - you can simply use it with useState() as you did in your child-component. E.g.:
...
const [selectedProducts, setSelectedProducts] = useState<Map<number, Product>>(new Map<number, Product>());
function onProductClick(product: Product): void {
selectedProducts.set(product.id, product);
// Note how we are re-setting the variable to make React aware of the change!
setSelectedProducts(new Map(selectedProducts));
}
...
It is important to note that Reacts useState hook compares values with tripple-equal (===) to determine whether it should trigger a re-render. Due to the nature of JavaScript (where variables only hold references to objects), setting a new value on a Map will not change its reference. To trigger a re-render you have to replace the Map.
Creating new objects is computationally cheap so this shouldn't cause any issues most of the times.

How do hooks know when to trigger (or not trigger) the body of useEffect?

Looking at the official react docs, an example is given for writing a custom hook, as below:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
function ChatRecipientPicker() {
const [recipientID, setRecipientID] = useState(1);
const isRecipientOnline = useFriendStatus(recipientID);
return (
<>
<Circle color={isRecipientOnline ? 'green' : 'red'} />
<select
value={recipientID}
onChange={e => setRecipientID(Number(e.target.value))}
>
{friendList.map(friend => (
<option key={friend.id} value={friend.id}>
{friend.name}
</option>
))}
</select>
</>
);
}
The part I'm confused about - my understanding is useEffect(...) will trigger each time the component re-renders, which as currently described, would only happen when setRecipientID is called. But, say another state variable is added, say, const [nameFilter, setNameFilter] = useState(''). In this case, the component will re-render every time a user types into the filter, which I think will trigger the "connection" logic in useEffect.
I think this would work if useEffect took in the friendID as the 2nd param, but being new to react I don't want to assume the official docs are not written in a resilient way, which means I'm wrong and the react plumbing somehow knows to not connect each re-render - but how?
To useEffect you need to provide a callback that is going to be executed and, optionally, an array of values that will determine when the effect is triggered. Here you have three options:
You pass nothing - useEffect is triggered each time components is rendered
You pass en empty array - useEffect is triggered only once, when component is mounted
You pass an array with props and state variables - useEffect is triggered when the component is first mounted and each time at least one of the variables changes
React does not do anything else to reduce the number of times useEffect is called, so yes you do need to explicitly provide variables your effect depends on (friendID in your case)

React: why is that changing the current value of ref from useRef doesn't trigger the useEffect here

I have a question about useRef: if I added ref.current into the dependency list of useEffect, and when I changed the value of ref.current, the callback inside of useEffect won't get triggered.
for example:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Also I know I can use useState here. This is not what I am asking. And also I know that ref stay referentially the same during re-renders so it doesn't change. But I am not doing something like
const myRef = useRef(1);
useEffect(() => {
//...
}, [myRef]);
I am putting the current value in the dep list so that should be changing.
I know I am a little late, but since you don't seem to have accepted any of the other answers I'd figure I'd give it a shot too, maybe this is the one that helps you.
Shouldn't it be when useRef.current changes, the stuff in useEffect gets run?
Short answer, no.
The only things that cause a re-render in React are the following:
A state change within the component (via the useState or useReducer hooks)
A prop change
A parent render (due to 1. 2. or 3.) if the component is not memoized or otherwise referentially the same (see this question and answer for more info on this rabbit hole)
Let's see what happens in the code example you shared:
export default function App() {
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<div className="App">
<button
onClick={() => {
myRef.current = myRef.current + 1;
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
</div>
);
}
Initial render
myRef gets set to {current: 1}
The effect callback function gets registered
React elements get rendered
React flushes to the DOM (this is the part where you see the result on the screen)
The effect callback function gets executed, "myRef current changed" gets printed in the console
And that's it. None of the above 3 conditions is satisfied, so no more rerenders.
But what happens when you click the button? You run an effect. This effect changes the current value of the ref object, but does not trigger a change that would cause a rerender (any of either 1. 2. or 3.). You can think of refs as part of an "effect". They do not abide by the lifecycle of React components and they do not affect it either.
If the component was to rerender now (say, due to its parent rerendering), the following would happen:
Normal render
myRef gets set to {current: 1} - Set up of refs only happens on initial render, so the line const myRef = useRef(1); has no further effect.
The effect callback function gets registered
React elements get rendered
React flushes to the DOM if necessary
The previous effect's cleanup function gets executed (here there is none)
The effect callback function gets executed, "myRef current changed" gets printed in the console. If you had a console.log(myRef.current) inside the effect callback, you would now see that the printed value would be 2 (or however many times you have pressed the button between the initial render and this render)
All in all, the only way to trigger a re-render due to a ref change (with the ref being either a value or even a ref to a DOM element) is to use a ref callback (as suggested in this answer) and inside that callback store the ref value to a state provided by useState.
https://reactjs.org/docs/hooks-reference.html#useref
Keep in mind that useRef doesn’t notify you when its content changes. Mutating the .current property doesn’t cause a re-render. If you want to run some code when React attaches or detaches a ref to a DOM node, you may want to use a callback ref instead.
use useCallBack instead, here is the explanation from React docs:
We didn’t choose useRef in this example because an object ref doesn’t
notify us about changes to the current ref value. Using a callback ref
ensures that even if a child component displays the measured node
later (e.g. in response to a click), we still get notified about it in
the parent component and can update the measurements.
Note that we pass [] as a dependency array to useCallback. This
ensures that our ref callback doesn’t change between the re-renders,
and so React won’t call it unnecessarily.
function MeasureExample() {
const [height, setHeight] = useState(0);
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
}, []);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}
Ok so I think what you're missing here is that changing a ref's value doesn't cause a re-render. So if it doesn't cause re-renders, then the function doesn't get run again. Which means useEffect isn't run again. Which means it never gets a chance to compare the values. If you trigger a re-render with a state change you will see that the effect will now get run. So try something like this:
export default function App() {
const [x, setX] = useState();
const myRef = useRef(1);
useEffect(() => {
console.log("myRef current changed"); // this only gets triggered when the component mounts
}, [myRef.current]);
return (
<button
onClick={() => {
myRef.current = myRef.current + 1;
// Update state too, to trigger a re-render
setX(Math.random());
console.log("myRef.current", myRef.current);
}}
>
change ref
</button>
);
}
Now you can see it will trigger the effect.

How to update state from the components in React?

I'm working on the "Orders list" page.
I have a hook:
const [orders, setOrders] = useState([])
What happens currently:
when page is loaded - I fetch orders with Apollo useQuery
in the useEffect: once I get orders, I set them with setOrders
then, I return them with:
{orders.map((order) => (
<OrderComponent order={order}/>
))}
This works fine for displaying data, but I would like to be able to change order data (just state) from OrderComponent.
So let's say we have a notes input in the OrderComponent:
<input type="text" name="order_notes" value={order.notes}/>
The question is:
How to handle such updates from the child component?
What I tried:
On the parent page, I created handler:
const handleOrderNotes = ({ value, orderIndex }) => {
const ordersCopy = [...orders]
ordersCopy[ordersIndex].notes = value
setOrders(ordersCopy)
}
then I passed it to the OrderComponent along with orderIndex:
{orders.map((order, index) => (
<OrderComponent order={order} orderIndex={index} handleOrderNotes={handleOrderNotes}/>
))}
And added the handler to the input:
<input
type="text"
name="order_notes"
value={order.notes}
onChange={(value) => {
handleOrderNotes({value, orderIndex})
}}
/>
And it worked just fine. But after I started adding more and more stuff, to the point where I would like to use components in OrderComponent. I started to wonder if there is a better way of doing such things?
During the devlopment I was often getting errors like:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
So I believe that I'm missing something crucial here.
Just getting started with React, so would apprentice any input. Thanks :)
There are two ways I can think of to solve this. You would have to chose which one suits you based on how your app is organized/designed.
If using redux or some kind of state management for app, then you don't need to tell parent component but as you have index of the order with you in the child component itself, you can simply update that order in the app store/state itself.
You may want to wrap each component with useMemo and use useCallback for passing any callback to these child components. This second step you should do even if you are have state management done for your app to avoid unnecessary re-renders
Please change this
{orders.map((order, index) => ( <OrderComponent order={order} orderIndex={index} handleOrderNotes={handleOrderNotes}/> ))}
to
{orders.map((order, index) => ( <OrderComponent order={order} orderIndex={index} handleOrderNotes={({value,index})=>handleOrderNotes({value,index})}/> ))}

Resources