In a React codebase I'm working on I have a custom hook which accepts a RefObject as a parameter, and an accompanying Provider to be used with such hook:
export const ScrollUtilsProvider = React.forwardRef<HTMLDivElement, ScrollUtilsProviderProps>(
(props, ref) => {
const scrollUtils = useScrollUtils(ref) // issue happens on this line
return <div ref={ref}><ScrollUtilsContext.Provider value={scrollUtils}>{props.children}</ScrollUtilsContext.Provider></div>
},
)
export const useScrollUtils = <T extends Element>(ref: RefObject<T>) => {
return {
// some cool functions w/ the passed ref
}
}
The error message I receive:
Argument of type 'ForwardedRef<HTMLDivElement>' is not assignable to parameter of type 'RefObject<HTMLDivElement>'.
Type 'null' is not assignable to type 'RefObject<HTMLDivElement>'.
Digging into both types I realised they are really different:
// from @types/[email protected]
interface RefObject<T> {
readonly current: T | null;
}
type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
My questions are:
interface RefObject<T> {
readonly current: T | null;
}
RefObject is the return type of the React.createRef method.
When this method is called, it returns an object with its only field .current set to null. Soon after, when a render passes the ref to a component, React will set .current to a reference to the component. This component will generally be a DOM element (if passed to a HTML element) or an instance of a class component (if passed to a custom class component).
Note that RefObject is very similar to MutableRefObject<T | null>, with the exception that .current is readonly. This type specification is only made to indicate that the .current property is managed internally by React and should not be modified by the code in a React app.
interface MutableRefObject<T> {
current: T;
}
MutableRefObject is the return type of the React.useRef method. Internally, React.useRef makes a MutableRefObject, stores it to the state of the functional component, and returns the object.
Note that when objects are stored to the state of a React component, modifying their properties will not trigger a re-render (since Javascript objects are reference types). This situation allows you to mimic class instance variables in functional components, which don't have instances. In other words, you can think of React.useRef as a way to associate a variable with a functional component without it affecting the component's renders.
Here's an example of a class component using instance variables and a functional component using React.useRef to achieve the same purpose:
class ClassTimer extends React.Component {
interval: NodeJS.Timer | null = null;
componentDidMount() {
this.interval = setInterval(() => { /* ... */ });
}
componentWillUnmount() {
if (!this.interval) return;
clearInterval(this.interval);
}
/* ... */
}
function FunctionalTimer() {
const intervalRef = React.useRef<NodeJS.Timer>(null);
React.useEffect(() => {
intervalRef.current = setInterval(() => { /* ... */ });
return () => {
if (!intervalRef.current) return;
clearInterval(intervalRef.current);
};
}, []);
/* ... */
}
type ForwardedRef<T> =
| ((instance: T | null) => void)
| MutableRefObject<T | null>
| null;
ForwardedRef is the type of ref React passes to functional components using React.forwardRef.
The main idea here is that parent components can pass a ref down to child components. For example, MyForm can forward a ref to MyTextInput, allowing the former to access the .value of the HTMLInputElement that the latter renders.
Breaking down the union type:
MutableRefObject<T | null> - The forwarded ref was created with React.useRef.
((instance: T | null) => void) - The forwarded ref is a callback ref.
null - No ref was forwarded.
When a child component receives a ForwardedRef, it is often to expose the ref to a parent. However, sometimes the child component may need to use the ref itself. In this case, you can use a hook to the reconcile each of the ForwardedRef types listed above.
Here is a hook from this article (adjusted for Typescript) that helps achieve this:
function useForwardedRef<T>(ref: React.ForwardedRef<T>) {
const innerRef = React.useRef<T>(null);
React.useEffect(() => {
if (!ref) return;
if (typeof ref === 'function') {
ref(innerRef.current);
} else {
ref.current = innerRef.current;
}
});
return innerRef;
}
The idea behind this hook is that the component can create its own ref, which it can use regardless of whether the parent forwarded a ref. The hook helps ensure that any forwarded ref's .current property is kept in sync with the inner one's.
The return type of this hook is MutableRefObject<T>, which should be compatible with the RefObject<T> argument in your code snippet for useScrollUtils, e.g.:
const MyComponent = React.forwardRef<HTMLDivElement>(
function MyComponent(_props, ref) {
const innerRef = useForwardedRef(ref);
useScrollUtils(innerRef);
return <div ref={innerRef}></div>;
}
);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With