In the React docs I have found this component that is supposed to be rendered only twice:
function Users() {
const [count, setCount] = useState(0);
console.log("---: " + count)
useEffect(() => {
console.log("useEffect")
const id = setInterval(() => {
console.log("setCount: " + (count + 1))
setCount(count + 1); // This effect depends on the `count` state
}, 1000);
return () => clearInterval(id);
}, []); // đź”´ Bug: `count` is not specified as a dependency
return <h1>{count}</h1>;
}
Try running previous component and you will get this logs where it is visible that a component is rerendered three times. So one unnecessary render happens (only one but still), even though setState has returned the same value. Can someone explain this, please?
Related
I've a hard time understanding how setInterval works. My main problem is that an interval is re-initialized too often.
Basically, I want a context-sensitive sidebar to be modfied by MainElement, and I want this sidebar to do something at a regular base. In the real scenario there the timer gets cancelled when unmounting ofc.
import { useEffect, useState } from 'react';
// This is the component called from outside
export const MainLayout = () => {
const [element2Content, setElement2Content] = useState<string | null>(null);
return (
<>
<MainElement setElement2Content={setElement2Content}>
Element1
</MainElement>
{element2Content && <Sidebar content={element2Content} />}
</>
);
};
// This component manipulates the sidebar via useEffect
const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
({ setElement2Content, children }) => {
useEffect(() => setElement2Content('content set from element 1'), []);
return <div>{children}</div>;
};
// This component utilizes the setInterval, but doesn't work as expected
const Sidebar: React.FC<{ content: string }> = ({ content }) => {
const [calls, setCalls] = useState(0);
useEffect(() => {
setInterval(() => {
console.log('interval called for', calls + 1, 'times');
setCalls(calls + 1);
}, 1000);
}, []);
return <div>{`content${content}, calls: ${calls}`}</div>;
};
The log is just interval called for 1 times in a loop.
In the browser I see the components rendered, and I see interval called for 0 times being changed to interval called for 1 times, where it stops.
So I'm wondering: Why does it stop at 1? It seems like setInterval gets reset all the time.
To understand the behavior of a timer a bit more, I changed my MainElement to
const MainElement: React.FC<{ setElement2Content: (input: string) => void }> =
({ setElement2Content, children }) => {
useEffect(() => setElement2Content('content set in element 1'), []);
useEffect(() => {
setInterval(() => {
console.log('interval called from mainelement');
}, 1000);
}, []);
return <div>{children}</div>;
};
Now, for some reason the MainElement is also re-rendered repeatedly, and so is the sidebar. The console logs
interval called in mainelement
interval called for 1 times
I'ld be grateful for any ideas or explanations!
The interval is running correctly, but in your interval function calls is never being updated passed 1. This is because the calls variable in the setInterval function is stale (i.e. it is always equal to 0 when the function is invoked, so setCalls(call + 1) will always update calls to 1.) This staleness is because the calls variable in the function is captured in the closure formed when the effect is run during the first render of the component and never updated thereafter.
A quick way to address this is to use the functional version of the setState call:
useEffect(() => {
setInterval(() => {
console.log('interval called for', calls + 1, 'times');
setCalls(prevCalls => prevCalls + 1);
}, 1000);
}, []);
In this very simple click counter example:
const App = () => {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count + 1)
}
return <button onClick={handleClick}>{count}</button>
}
My understanding of the flow is:
Component gets mounted with count=0
When button is clicked, new count state is set with increment of 1.
That triggers a re-render, so now I have count=1, and repeats.
However, using the same understanding, why does it not work here?
const App = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1)
}, 500)
return () => {
clearInterval(timer)
}
}, [])
console.log(count) // this stops at 1, so the timer stops triggering??
return <h1>{count}</h1> // get stuck at 1
}
The outcome of the above code is that the count gets stuck at value 1. (and weirdly the console.log stops too).
I thought each time the setInterval timer triggers, the count increment will cause a re-render with new count value and therefore it will just increase by 1 forever?
The fix here is to simply pass in a function argument to access prevState:
const timer = setInterval(() => {
setCount(oldCount => oldCount + 1)
}, 500)
But why couldn't the first approach work?
Hope someone can point me to a good article or documentation of this, I tried searching around but couldn't get any explanation.
The first approach doesn't work because with this
setCount(count + 1)
You are creating a copy of the count value at that particular time you created the callback. This means that every 500ms you will re-execute this line
setCount(0 + 1)
That won't cause a re-render because react is intelligent enough to understand that you are passing the same value to the setCount function so the re-render would not be necessary.
However by passing a callback to setCount:
setCount(oldCount => oldCount + 1)
You are saying that you want the current value of the state count, so each time the argument of that function will be different and therefore it will cause a re-render.
You can find a doc about this topic here: https://reactjs.org/docs/hooks-reference.html#functional-updates
This console.log is not working: It'll just print the previous state value as set is async.
const SomeCompo = () => {
const [count, set] = useState(0);
const setFun = () => {
console.log(count);
set(count + 1);
console.log(count);
}
return <button onClick={setFun}>count: {count}</button>
}
I had to read the count in the render itself:
const SomeCompo = () => {
const [count, set] = useState(0);
console.log(count);
const setFun = () => {
set(count + 1);
}
return <button onClick={setFun}>count: {count}</button>
}
Is there a better way to read the value as I don't want to console for every render.
You can use useEffect for this,
useEffect(() => {
console.log(count);
}, [count]) //[count] is a dependency array, useEffect will run only when count changes.
I would suggest not to use setInterval. I would do something like useEffect. This function will be called each time you do a setState. Just like you had callback after setState. Pass the count state in the array, it will watch only for the count change in the state and console your count.
useEffect(() => {
console.log(count);
}, [count]);
Also if you dont need to rerender your other components, you might wanan use useMemo and useCallback. https://www.youtube.com/watch?v=-Ls48dd-vJE
Here to more read: https://reactjs.org/docs/hooks-effect.html
The way to get a state value is to use useEffect and use the state as a dependency. This means that when we change a value the render cycle will finish and a new one will start, then useEffect will trigger:
useEffect( () => { console.log(value); }, [value] );
If you would need to read the value in the same cycle as it is changed a possibility could be to use the useState set function. This shows the latest value just before updating it:
setValue( latest_value => {
const new_value = latest_value + 1;
console.log(new_value);
return new_value;
} );
I am running into issues setting a state created with the 'useState' hook from within async functions.
I've created a codepen to demonstrate: https://codepen.io/james-ohalloran/pen/ZdNwWQ
const Counter = () => {
const [count, setCount] = useState(0);
const increase = () => {
setTimeout(() => {
setCount(count + 1);
},1000);
}
const decrease = () => {
setTimeout(() => {
setCount(count - 1);
},1000)
};
return (
<div className="wrapper">
<button onClick={decrease}>-</button>
<span className="count">{count}</span>
<button onClick={increase}>+</button>
</div>
);
};
In the above example, if you click 'increase' followed by 'decrease'..you will end up with -1 (I would expect it to be 0).
If this was a React class instead of a functional component, I would assume the solution would be to use bind(this) on the function, but I didn't expect this to be an issue with arrow functions.
It is because of using setTimeout
Let's assume that you've called the increase() 10 times in a second.
count will be always 0. Because the state is updated after a second, every increment() called in a second will have an unupdated count.
So every increment() will call setCount(0 + 1);.
So no matter how many times you call in a second, the count is always 1.
Ah, I found a solution. I didn't realize I'm able to reference the previousState from the useState setter function: https://reactjs.org/docs/hooks-reference.html#functional-updates
Is there any alternative to just keeping a "clock" in the background to implement auto-next (after a few seconds) in carousel using react hooks?
The custom react hook below implements a state for a carousel that supports manual (next, prev, reset) and automatic (start, stop) methods for changing the carousel's current (active) index.
const useCarousel = (items = []) => {
const [current, setCurrent] = useState(
items && items.length > 0 ? 0 : undefined
);
const [auto, setAuto] = useState(false);
const next = () => setCurrent((current + 1) % items.length);
const prev = () => setCurrent(current ? current - 1 : items.length - 1);
const reset = () => setCurrent(0);
const start = _ => setAuto(true);
const stop = _ => setAuto(false);
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, 3000);
return _ => clearInterval(interval);
});
return {
current,
next,
prev,
reset,
start,
stop
};
};
There are differences between setInterval and setTimeout that you may not want to lose by always restarting your timer when the component re-renders. This fiddle shows the difference in drift between the two when other code is also running. (On older browsers/machines—like from when I originally answered this question—you don't even need to simulate a large calculation to see a significant drift begin to occur after only a few seconds.)
Referring now to your answer, Marco, the use of setInterval is totally lost because effects without conditions dispose and re-run every time the component re-renders. So in your first example, the use of the current dependency causes that effect to dispose and re-run every time the current changes (every time the interval runs). The second one does the same thing, but actually every time any state changes (causing a re-render), which could lead to some unexpected behavior. The only reason that one works is because next() causes a state change.
Considering the fact that you are probably not concerned with exact timing, is is cleanest to use setTimeout in a simple fashion, using the current and auto vars as dependencies. So to re-state part of your answer, do this:
useEffect(
() => {
if (!auto) return;
const interval = setTimeout(_ => {
next();
}, autoInterval);
return _ => clearTimeout(interval);
},
[auto, current]
);
Generically, for those just reading this answer and want a way to do a simple timer, here is a version that doesn't take into account the OP's original code, nor their need for a way to start and stop the timer independently:
const [counter, setCounter] = useState(0);
useEffect(
() => {
const id= setTimeout(() => {
setCounter(counter + 1);
// You could also do `setCounter((count) => count + 1)` instead.
// If you did that, then you wouldn't need the dependency
// array argument to this `useEffect` call.
}, 1000);
return () => {
clearTimeout(id);
};
},
[counter],
);
However, you may be wondering how to use a more exact interval, given the fact that setTimeout can drift more than setInterval. Here is one method, again, generic without using the OP's code:
// Using refs:
const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
() => {
const id = setInterval(() => {
r.current.setCounter(r.current.counter + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
// Using the function version of `setCounter` is cleaner:
const [counter, setCounter] = useState(30);
useEffect(
() => {
const id = setInterval(() => {
setCounter((count) => count + 1);
}, 1000);
return () => {
clearInterval(id);
};
},
[] // empty dependency array
);
Here is what is going on above:
(first example, using refs): To get setInterval's callback to always refer to the currently acceptable version of setCounter we need some mutable state. React gives us this with useRef. The useRef function will return an object that has a current property. We can then set that property (which will happen every time the component re-renders) to the current versions of counter and setCounter.
(second example, using functional setCounter): Same idea as the first, except that when we use the function version of setCounter, we will have access to the current version of the count as the first argument to the function. No need to use a ref to keep things up to date.
(both examples, continued): Then, to keep the interval from being disposed of on each render, we add an empty dependency array as the second argument to useEffect. The interval will still be cleared when the component is unmounted.
Note: I used to like using ["once"] as my dependency array to indicate that I am forcing this effect to be set up only once. It was nice for readability at the time, but I no longer use it for two reasons. First, hooks are more widely understood these days and we have seen the empty array all over the place. Second, it clashes with the very popular "rule of hooks" linter which is quite strict about what goes in the dependency array.
So applying what we know to the OP's original question, you could use setInterval for a less-likely-to-drift slideshow like this:
// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...
const r = useRef(null);
r.current = { next };
useEffect(
() => {
if (!auto) return;
const id = setInterval(() => {
r.current.next();
}, autoInterval);
return () => {
clearInterval(id);
};
},
[auto]
);
Because the current value is going to change on every "interval" as long as it should be running, then your code will start and stop a new timer on every render. You can see this in action here:
https://codesandbox.io/s/03xkkyj19w
You can change setInterval to be setTimeout and you will get the exact same behaviour. setTimeout is not a persistent clock, but it doesn't matter since they both get cleaned up anyways.
If you do not want to start any timer at all, then put the condition before setInterval not inside of it.
useEffect(
() => {
let id;
if (run) {
id = setInterval(() => {
setValue(value + 1)
}, 1000);
}
return () => {
if (id) {
alert(id) // notice this runs on every render and is different every time
clearInterval(id);
}
};
}
);
So far, it seems that both solutions below work as desired:
Conditionally creating timer — it requires that useEffect is dependent both on auto and current to work
useEffect(
() => {
if (!auto) return;
const interval = setInterval(_ => {
next();
}, autoInterval);
return _ => clearInterval(interval);
},
[auto, current]
);
Conditionally executing update to state — it does not require useEffect dependencies
useEffect(() => {
const interval = setInterval(_ => {
if (auto) {
next();
} else {
// do nothing
}
}, autoInterval);
return _ => clearInterval(interval);
});
Both solutions work if we replace setInterval by setTimeout
You could use useTimeout hook that returns true after specified number of milliseconds.
https://github.com/streamich/react-use/blob/master/docs/useTimeout.md