Reputation: 3307
Consider I have a custom hook that uses one or more refs
to DOM elements, I see two distinct ways how to write/use a hook like that (and I know there are probably even more nuances – e. g. callback refs).
Option 1: return a ref
from custom hook and use it in a component:
const Option1 = () => {
const hooksRef = myCustomHookReturninARef()
return <div ref={hooksRef}>Using returned ref as prop.</div>
}
Option 2: create a ref
in a component and pass it to a custom hook:
const Option2 = () => {
const componentsRef = React.createRef()
myCustomHookExpectingRefArgument(componentsRef)
return <div ref={componentsRef}>Using created ref as prop..</div>
}
I've been using either options and they both seems to be working fine. I know this is likely an opinionated question, but:
Are there some significant drawbacks using the first vs the second approach?
Upvotes: 10
Views: 4620
Reputation: 40459
I went this direction:
import {useMemo} from 'react';
type CallbackRef<T> = (ref: T | null) => void;
type Ref<T> = React.MutableRefObject<T> | CallbackRef<T>;
function toFnRef<T>(ref?: Ref<T> | null) {
if (!ref || typeof ref === 'function') {
return ref;
}
return (value: T) => {
ref.current = value;
};
}
function mergeRefs<T>(refA?: Ref<T> | null, refB?: Ref<T> | null) {
const a = toFnRef(refA);
const b = toFnRef(refB);
return (value: T | null) => {
if (a) {
a(value);
}
if (b) {
b(value);
}
};
}
/**
* Create and returns a single callback ref composed from two other Refs.
*
* @param refA A Callback or mutable Ref
* @param refB A Callback or mutable Ref
* @category refs
*/
export function useMergedRef<T>(refA?: Ref<T> | null, refB?: Ref<T> | null) {
return useMemo(() => mergeRefs(refA, refB), [refA, refB]);
}
Upvotes: 0
Reputation: 113
The important detail is to pick one pattern and stick to it. Mixing the 2 patterns in your hooks could create a lot of pain down the line.
I lean towards option 1 - creating the ref in the custom hook. This is mainly because having the ref within the custom hook provides more flexibility, like setting it up as a callback ref.
The useHooks repo also follows the 1st pattern for their hooks. All their hooks generate refs internally and do not get the ref from the parent.
We ran into this issue recently. We had 2 hooks being used. The way we designed it was that the first hook would return a ref that we could pass into the 2nd hook.
const [hover, ref] = useHover();
useDetectClickOutside(ref)
return <div ref={ref}> ...
This worked fine for a while but later we ran into issues when the element being bound to the ref was conditionally being rendered. We wanted to change the useHover
hook to use a callback ref which will allow us better control whenever the element changes or is removed.
Making this change however broke the useDetectClickOutside
hook which expected a ref and not a callback function as the parameter.
Facing this we decided to standardize our custom hooks and always create the ref within the hook and pass it to the parent. We abstracted the way the hooks are merged so that we could handle both regular ref and callback refs.
const [hover, hoverRef] = useHover();
const clickRef = useDetectClickOutside(ref)
return <div ref={mergeRefs(hoverRef, clickRef)}> ...
function mergeRefs(...refs) {
return (node) => {
refs.forEach((ref) => {
if (!ref) return;
if (typeof ref === "function") {
ref(node);
} else {
ref.current = node;
}
});
};
}
Upvotes: 1
Reputation: 153
Really, it depends. When working with both Option 1 and Option 2, you would have to create extra variables and functions, depending on the use case, though those are typically negligible.
When it comes to performance, I would say they're virtually identical.
Personally, I'm much more of a fan of the modularity of Option 2, for these reasons:
useEffect
call). Consider the following:export default () => {
const ref = React.createRef<HTMLDivElement>();
React.useEffect(() => {
if(ref.current){
ref.current.style.width = "120px";
}
}, []}
const myCustomHook = useMyCustomHook(ref);
return <div ref={ref} />
}
Of course, this functionality could be built into the hook to begin with, with a little foresight:
const useMyCustomHook = (ref: React.RefObject, mutateRefCallback: () => any {
React.useEffect(() => {
mutateRefCallback();
}, []}
// ...do other stuff
}
export default () => {
const ref = React.createRef<HTMLDivElement>();
const myCustomHook = useMyCustomHook(ref, () => {
if(ref.current){
ref.current.style.width = "120px";
}
});
return <div ref={ref} />
}
But my opinion is that the latter creates unnecessarily complex code for what typically is a rare case.
// Option 1
const useMyFirstCustomHook = (inputRef: React.RefObject<HTMLInputElement>) =>
{
//...do stuff
return inputRef;
}
const useMySecondCustomHook = (buttonRef: React.RefObject<HTMLButtonElement>) => {
//...do stuff
return buttonRef;
}
type MyInputProps = {
id: string;
children?: string;
placeholder?: string;
}
const MyInput = React.forwardRef((props: MyInputProps, ref) => {
const {children, placeholder} = props;
return (
<label htmlFor={id}>
{children}
<input id={id} ref={ref} placeholder={placeholder} />
</label>
)
});
const MyForm = () => {
/* Downside: Declaring refs and calling hooks separately
leads to sometimes unnecessary code splitting. */
const inputRef = useMyFirstCustomHook();
const buttonRef = useMySecondCustomHook();
return(
<form>
<MyInput placeholder="John Doe" ref={inputRef}>Name:</MyInput>
<button type="button"
onClick={() => {
//..do stuff
}}
ref={buttonRef}>Save</button>
</form>
)
}
Since with Option 1 we can only have a single hook consuming a single ref at a time, our code becomes unnecessarily complicated. Here's an approach with Option 2:
const useMyCustomHook = (
inputRef: React.RefObject<HTMLInputElement>,
buttonRef: React.RefObject<HTMLButtonElement>) =>
{
//...do stuff
return {inputRef, buttonRef};
}
type MyInputProps = {
id: string;
children?: string;
placeholder?: string;
}
const MyInput = React.forwardRef((props: MyInputProps, ref) => {
const {children, placeholder} = props;
return (
<label htmlFor={id}>
{children}
<input id={id} ref={ref} placeholder={placeholder} />
</label>
)
});
const MyForm = () => {
const inputRef = React.createRef<HTMLInputElement>();
const buttonRef = React.createRef<HTMLButtonElement>();
const myCustomHook = useMyCustomHook(inputRef, buttonRef);
return(
<form>
<MyInput placeholder="John Doe" ref={inputRef}>Name:</MyInput>
<button type="button"
onClick={() => {
//..do stuff
}}
ref={buttonRef}>Save</button>
</form>
)
}
Furthermore, Option 2 lets us mutate the refs based on one another a lot more easily.
Note that both approaches are valuable in their own way.
While Option 2 may be more modular, intuitive and clean in some regards, Option 1 is oftentimes a lot more straightforward and easy to use in common situations, such as a simple side-effect on mount.
Upvotes: 0
Reputation: 2399
I came here because I myself have the same dilemma :-). My 2 cents:
Maybe one could get the best of both worlds, by both receiving and returning a ref? If the hook doesn't get a ref then it simply creates one. Either way it can return the ref.
Upvotes: 5
Reputation: 3856
not an React virtuoso, but from my perspective the only drawback using Option1
is that you have to validate the returned value to be a ref
, from computing perspective there is no difference
Upvotes: 0