-
Notifications
You must be signed in to change notification settings - Fork 47.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lazy useRef instance variables #14490
Comments
@Diokuz useRef does take in an initial state. https://reactjs.org/docs/hooks-reference.html#useref In the above link, they just pass |
@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 |
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? |
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 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! |
How about: const [myRef] = useState(() => ({ current: calculateInitialValue() })) Is there any reason not to "abuse" |
// @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; |
I second @ghengeveld's question above about "abusing" |
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 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. |
Usually we want to change The But ideally, React should just improve |
fwiw, published a hook for this under the npm package src: https://github.com/alii/alistair/blob/master/src/hooks/use-lazy-ref.ts |
Is this still the recommended approach? I am getting bugs with react-refresh hot updates, when I'm using lazy ref init with a Eg. if (!editorRef.current) {
editorRef.current = new Editor();
}
useEffect(() => {
return () => {
if (editorRef.current) {
editorRef.current.destroy();
editorRef.current = null;
}
};
}, []); During hot reload, |
The problem with your example is that your effect is not symmetric: your cleanup is doing a different thing from your setup. So this breaks Fast Refresh, and will also break in Strict Mode in 18 which double-calls effects to check whether your component is ready for reusable state. You could do this to make it symmetric: useEffect(() => {
let editor = editorRef.current = new Editor();
return () => {
editorRef.current = null;
editor.destroy();
};
}, []); It's hard to say more without a runnable example. |
Thanks @gaearon. I have indeed moved ref initialization into In general, the solution with useEffect isn't 100% ideal because you don't have the ref instance available during the initial render. However, I was able to work around that in my use case. For curiosity's sake, here's a sandbox with the repro of what I think was happening: https://codesandbox.io/s/musing-kowalevski-3omfcs?file=/src/App.js You can trigger hot refresh by adding a comment or similar. |
That’s right. But that’s on purpose. Renders in React are supposed to be discardable. For example, with Suspense, if a newly mounting part of the tree suspends, React 18 will throw it away and add a placeholder instead. That tree never had a chance to mount so we don’t fire its cleanup. So if your instance requires to be destroyed you would have a memory leak. Delaying its creation to effect avoids that problem. Another way to avoid that problem is to split the external thing apart. The part that doesn’t consume resources (which is free to discard) is created during first render. So it’s safe to throw away. The part that consumes resources (for example, creates a subscription) is created in the effect. So it has a symmetric cleanup. Hope this helps. |
@gaearon Understood. Thank you. |
This seems like a common requirement so is there a reason why we can't update |
This uses lazy refs instead of standard refs to lazily instantiate plugins and ensure they only get called once. This method is recommended over `useMemo` because it's not designed to retain values, and could recompute arbitrarily in the future (see facebook/react#14490 (comment)).
This uses lazy refs instead of standard refs to lazily instantiate plugins and ensure they only get called once. This method is recommended over `useMemo` because it's not designed to retain values, and could recompute arbitrarily in the future (see facebook/react#14490 (comment)).
A bug occurs when logs are already loading, and an accidental simultaneous scroll event triggers another log pull. The second pull will append the same logs twice. According to the previous work, we can already sequentialize the pulls using a queue. So the fix is to avoid the second pull being issued if we discovered from the first pull that there would be no more logs. This PR does such, and some extras: 1. Add more loggings to understand the async queuing and execution details 2. A helper function `useRefFn` to avoid the initial value of `useRef` being re-created multiple times across rendering. (see also [here](facebook/react#14490)) 3. Convert `AsyncInvocationQueue` from a function to a class to carry more responsibilities 4. Updated unit test accordingly. --------- Co-authored-by: tscurtu <[email protected]>
@gaearon I'm still unclear what "problem" will be caused if we immediately initialize the reference as @TrySound suggested: const instance = React.useRef(null)
if (instance.current == null) {
instance.current = {
// whatever you need
}
} Will that cause any potential issues? Or were you comparing the "delay creation until effect" approach with something else? The benefit of this approach, if we know we'll need the instance, is that we've consolidated creation in one place, as we may have multiple consumers/users of the instance and it's error-prone to rely on the users to generate the data when they need it, it results in duplicated initialization code, it spreads initialization code around, etc. |
@garretwilson because your ref has the wrong type.... @everyone_else: Use this when your ref initializers never return null. // useLazyRef.ts
import { useRef } from 'react';
function useLazyRef<T>(initializer: () => (null extends T ? never : T)) {
const ref1 = useRef<T | null>(null);
if (ref1.current === null) {
ref1.current = initializer();
}
const ref2 = useRef(ref1.current);
return ref2;
}
export default useLazyRef; Usage: import useLazyRef from '@/useLazyRef';
// ...
const numberRef1 = useLazyRef(() => 123);
// good: numberRef1 is of type MutableRefObject<number>
const numberRef2 = useLazyRef<number>(() => 123);
// good: numberRef2 is of type MutableRefObject<number>
const failRef1 = useLazyRef(() => null);
// good: TS error
const failRef2 = useLazyRef<boolean>(() => 123);
// good: TS error |
A bug occurs when logs are already loading, and an accidental simultaneous scroll event triggers another log pull. The second pull will append the same logs twice. According to the previous work, we can already sequentialize the pulls using a queue. So the fix is to avoid the second pull being issued if we discovered from the first pull that there would be no more logs. This PR does such, and some extras: 1. Add more loggings to understand the async queuing and execution details 2. A helper function `useRefFn` to avoid the initial value of `useRef` being re-created multiple times across rendering. (see also [here](facebook/react#14490)) 3. Convert `AsyncInvocationQueue` from a function to a class to carry more responsibilities 4. Updated unit test accordingly. --------- Co-authored-by: tscurtu <[email protected]>
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:
But
useRef
does not accept the initial value as a function (useState
anduseReducer
could do that).Will that change in alpha 3?
The text was updated successfully, but these errors were encountered: