349989153 / 349989153.github.io

My personal blog.
0 stars 0 forks source link

在forwardRef里面使用ref #18

Open 349989153 opened 3 years ago

349989153 commented 3 years ago

链接:https://itnext.io/reusing-the-ref-from-forwardref-with-react-hooks-4ce9df693dd

349989153 commented 3 years ago

Reusing the ref from forwardRef with React hooks

Daniel 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>

Problem

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?— 🤷‍♂️

Solution

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!