I want to save a class instance, not just plain object, with useRef
hook. And, I dont want to construct it each time hook function is invoked.
Is it possible to do that?
My thoughts were:
const instanceRef = useRef(() => new Instance())
But useRef
does not accept the _initial value_ as a function (useState
and useReducer
could do that).
Will that change in _alpha 3_?
@Diokuz useRef does take in an initial state.
https://reactjs.org/docs/hooks-reference.html#useref
In the above link, they just pass null
as the initial value.
@nikilok yes, but the question was about how to create lazy initial value (create it only once).
My current solution is:
const [refInitialValue] = useState(() => new Instance)
const instanceRef = useRef(refInitialValue)
but that is a bit overhead.
@Diokuz Just check its existence
const instance = React.useRef(null)
if (instance.current == null) {
instance.current = {
// whatever you need
}
}
I created this custom hook to help with this limitation, someone might find it useful:
import { useRef } from 'react';
const SENTINEL = {};
export function useRefFn<T>(init: () => T) {
const ref = useRef<T | typeof SENTINEL>(SENTINEL);
if (ref.current === SENTINEL) {
ref.current = init();
}
return ref as React.MutableRefObject<T>;
}
It happens to work in this case because your function is generic and so T is "like {}", but you need to use a Symbol for sentinels to create a unique symbol
type. Using an empty object's type as an input makes it nearly the same as any
but without null | undefined
.
Sorry, I'm not following. It would work for null, undefined, or any value as far as I can see.
I use an empty object as the sentinel as it makes it unique and distinct for the === check which sets the ref to the real initial value on the first invocation. It'd work it the sentinel were a symbol too, but I don't think it makes a difference one way or the other?
Late realisation: doesn't useMemo (with an empty array as the second parameter) do exactly this?
useMemo
with []
is not recommended for this use case. In the future we will likely have use cases where we drop useMemo
values — to free memory or to reduce how much we retain with e.g. virtual scrolling for hidden items. You shouldn't rely on useMemo
retaining a value.
We talked more about this and settled on this pattern as the recommendation for expensive objects:
function Foo() {
const instanceRef = useRef(null)
function getInstance() {
let instance = instanceRef.current;
if (instance !== null) {
return instance;
}
// Lazy init
let newInstance = new Instance()
instanceRef.current = newInstance;
return newInstance;
}
// Whenever you need it...
const instance = getInstance();
// ...
}
Note how you can declare getInstance()
as : Instance
— it's easy to guarantee it's never null
.
So you both get type safety and delay creation until the object is actually necessary (which may happen conditionally during rendering, or during an effect, etc).
Hope this helps!
https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
How about:
const [myRef] = useState(() => ({ current: calculateInitialValue() }))
Is there any reason not to "abuse" useState
like that?
// @flow
import {useRef} from "react";
type HookRef<Value> = {|
current: Value
|};
const noValue = Symbol("lazyRef.noValue");
const useLazyRef = <Value>(getInitialValue: () => Value): HookRef<Value> => {
const lazyRef = useRef(noValue);
if (lazyRef.current === noValue) {
lazyRef.current = getInitialValue();
}
return lazyRef;
};
export default useLazyRef;
The benefit of Dan's approach is that the instance is created once, at the point where it's going to be used (if at all), while my solution just instantiates/creates it immediately (also only once). I think my pattern is fine if you know for certain you are going to need the value. Unless there is a reason to avoid this trick, like there is for useMemo?
You can also just pass a function that mutates a ref to useMemo
without being at risk of changing semantics WRT retained values:
const initializeRef = (ref: React.MutableRefObject<any>) => {
if (!ref.current) {
ref.current = init();
}
}
// in your component
const ref = useRef(null)
useMemo(() => {
initializeRef(ref)
}, [])
The handy thing about this is that you can also re-run initialisation of the ref if needed by supplying deps.
Most helpful comment
@Diokuz Just check its existence