Open 349989153 opened 3 years ago
ref
from forwardRef
with React hooksDaniel Ostapenko Daniel Ostapenko Follow Aug 26, 2019 · 3 min read
I guess, we all started already to use new cool features from React v16. One of them is the new way to forward refs
to our components. The common syntax for that is:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
))
// You can now get a ref directly to the DOM button:
const ref = React.createRef()
<FancyButton ref={ref}>Click me!</FancyButton>
Easy, right?
But what if you need to reuse the ref
for your needs inside the forwardable component? This issue has turned out to be not as easy to solve as supposed to be.
Let's imagine we have an input
which is used inside our component MeasuredInput . And we need to measure the dimensions of that input
using ref , but also, we want MeasuredInput
component to forward the ref to the child input
component. So let's write some code:
const MeasuredInput = forwardRef((props, ref) => {
// measure here dimensions of <input />
return (
<input ref={ref} />
)
})
So now to implement the measurement we need to use ref
. Let's do that without taking into account the ref
forwarding:
const MeasuredInput = props => {
const ref = React.useRef(null)
React.useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect()
console.log('Input dimensions:', rect.width, rect.height)
}, [ref])
return (
<input ref={ref} />
)
}
And what will happen if we want to forward the ref from the outside of MeasuredInput
?— 🤷♂️
The ref
object is just an object with the shape of { current: null }
as we know from the React documentation https://reactjs.org/docs/hooks-reference.html#useref. And current
is just a reference to a DOM node. So ideally we want to reuse this reference which comes from the forwardRef
inside the MeasuredInput
. Like this:
const MeasuredInput = React.forwardRef((props, ref) => {
const innerRef = React.useRef(ref)// set ref as an initial value
React.useLayoutEffect(() => {
const rect = innerRef.current.getBoundingClientRect()
console.log('Input dimensions:', rect.width, rect.height)
}, [ref])
return (
<input ref={innerRef} />
)
})
But sadly it doesn't work like that :) The ref
from outside stays { current: undefined }
and not being updated with the value of the reference of the input .
To fix that we need to write some manual update function for the ref
and merge those refs to use the single reference value:
function useCombinedRefs(...refs) {
const targetRef = React.useRef()
React.useEffect(() => {
refs.forEach(ref => {
if (!ref) return
if (typeof ref === 'function') {
ref(targetRef.current)
} else {
ref.current = targetRef.current
}
})
}, [refs])
return targetRef
}
const MeasuredInput = React.forwardRef((props, ref) => {
const innerRef = React.useRef(null)
const combinedRef = useCombinedRefs(ref, innerRef)
React.useLayoutEffect(() => {
const rect = combinedRef.current.getBoundingClientRect()
console.log('Input dimensions:', rect.width, rect.height)
}, [ref])
return (
<input ref={combinedRef} />
)
})
This will work exactly as expected. Honestly, I don't think that it should stay for a while, because the solution is quite big and definitely not easy to understand and hard to maintain. So I hope React maintainers will find a better API soon. Enjoy!
链接:https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd