I am using a custom hook with a timer to update a Context Provider. I then wrap the Provider component around the components that need the timer.
export const useTimer = () => {
const [a, setA] = useState()
const [b, setB] = useState()
const [c, setC] = useState()
// Start the timer
useEffect(() => {
const interval = setInterval(() => {
// Do some calculations on a, b, c
// a, b & c change independently of each other
// but not necessarily at each interval:
// sometimes they don't change at all,
// other times only one or two of them changes,
// sometimes all three.
// then setA(someValue), setB(someValue) etc
}, 30)
return () => clearInterval(interval)
}, [])
return [a, b, c]
}
export const TimerContext = createContext()
export const TimerProvider = ({ children }) => {
return (
<TimerContext.Provider value={useTimer()}>
{children}
</TimerContext.Provider>
)
}
Because I’m returning a new array from useTimer
every time, it updates the Provider every 30ms, not when the values change. This causes unnecessary rerenders in the wrapped components. If I return an object instead the result is the same, because it’s a new object. And I can’t return multiple independent variables in JavaScript.
Even if I memoize the array in the timer, the TimerContext
sees the return value as having changed and is retriggered, I don’t know why.
const time = useMemo(() => ([a, b, c]), [a, b, c])
return time
Wrapping TimerProvider
inside React.memo
doesn’t help either.
Moreover, some components only need a
and b
, while others only need c
. I’d like to create independent Contexts, all based on a single running timer, which only update when the needed values have changed.
All values are cyclic, although of different cycles. The cycles are known, though. I guess I could encode the values into a single integer but this seems a bit insane to do every 30ms and also seems overly complex for such a simple requirement.
How can I get around this?
3
Answers
Answering my own question in case it helps someone. After many attempts I've found a solution that does exactly what I need, although I can't yet tell how it performs CPU-wise, I'll have to make some tests.
The idea is not to memoize the return object from the timer, but rather to memoize inside the
TimerProvider
component each combination of values you need to monitor and then switch between them, passing the required value to the Context.This allows the usage of a single generic
TimerProvider
which can wrap any component that needs a combination of the values by passing it, as prop, a string with the required configuration.I am still very much interested in a more elegant and possibly more performant solution.
Example usage:
Yes, you can do that; there’s no reason that you have to feed the return-value of
useTimer()
directly into a context.That also addresses your issue with the array being different every time, since you don’t pass the actual array into the context.
That’s very strange. Is it possible that one of the values is actually changing every time, without your realizing it? That would defeat the memoization, obviously.
The main problem with React it’s state manager is only at the component level, when you want to distribute state it get’s messy, using things like
React.memo
etc, is often used to prevent performance issues, when in most cases it’s the way state is used that’s the issue. I have React apps now with hundreds of components, accessing remote DB’s etc, and have yet not found any reasons to useReact.memo
,useMemo / useCallback
etc.As I mentioned in comments it’s often best to use a third party state manager when you need more than just simply component state.
One thing to always remember about React’s rendering pipeline, it’s granularity is as the component level, so the more you keep components small the better.
Yes, all we have to do is use
useState
differently. Steeling the idea of other state managers like https://jotai.org/ we could create a very simple atomic wrapper for the state.Basically the
createAtom
function uses the browsers built in EventTarget to distribute state.Below is a simple example, you will notice in the console how the components are only getting rendered when it’s particular value changes. I’ve also done 2 renders for and 2 separate contexts to see how you can pass the state down using
useContext
.