Skip to content
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

Refs merging/combining #29757

Open
FrameMuse opened this issue Jun 4, 2024 · 5 comments
Open

Refs merging/combining #29757

FrameMuse opened this issue Jun 4, 2024 · 5 comments

Comments

@FrameMuse
Copy link

Summary

I wanted to use ref from props while also creating a local ref as a fallback, but I got into this problem #17200.

I know this could be resolved with a little code, though I think this behavior should belong natively to React. There is even a package for that https://www.npmjs.com/package/react-merge-refs with ~1 million downloads/week, which confirms necessity of this for the React users.

I would propose something like

function Component(props: { ref?: Ref<HTMLDivElement> }) {
  const fallbackRef = useRef<HTMLDivElement>(null)
  return <div ref={[props.ref, fallbackRef]} />
}

or at least this for fallback cases

function Component(props: { ref?: Ref<HTMLDivElement> }) {
  const elementRef = useRef<HTMLDivElement>(props.ref)
  return <div ref={elementRef} />
}

or hook/helper to resolve refs concurrency

function Component(props: { ref?: Ref<HTMLDivElement> }) {
  const elementRef = useRef<HTMLDivElement>(null)
  const combinedRef = useCombinedRef([props.ref, elementRef])

  return <div ref={combinedRef} />
}
function Component(props: { ref?: Ref<HTMLDivElement> }) {
  const elementRef = useRef<HTMLDivElement>(null)
  return <div ref={combineRefs([props.ref, elementRef])} />
}

What do you think? Am I going too far with it? 😅

@monikkacha
Copy link

Hey @FrameMuse, I checked your reference npm package and whole scenario and it seems like worth adding, but I haven't gotten around to such a scenario as when I need to have a ref for parent and child both component, can you please explain the use case further more so I can better understand the requirement.

I am willing to work on this feature and raise PR if the scenario is really useful and maintainers are happy to add this feature.

@FrameMuse
Copy link
Author

FrameMuse commented Jun 6, 2024

@monikkacha

Here's two use-cases and my real-world example, hope this would be helpful.

Fallback

In React 19 I can use ref in props, put in an element and use it in the component itself while giving an outside access to it as well.

function Component(props: { ref: Ref<HTMLDivElement> }) {
  useClickAway(props.ref)
  return <div ref={props.ref} />
}

function Parent() {
  const elementRef = useRef<HTMLDivElement>(null)
  useLayoutEffect(() => {
    elementRef.current.clientHeight // Will actually work now.
  }, [])
  return <Component ref={elementRef} />
}

But the problem comes when you want to make that ref to be optional.

function Component(props: { ref?: Ref<HTMLDivElement> }) {
  useClickAway(props.ref) // Won't work if `ref` is not passed.
  return <div ref={props.ref} />
}

// Click Away will work only for component that is receiving a ref.
<Component ref={elementRef} />
<Component />

That's why we need to use a fallback ref.

function Component(props: { ref?: Ref<HTMLDivElement> }) {
  const elementRef = useRef<HTMLDivElement>(null)
  const combinedRef = combineRefs([elementRef , props.ref])

  useClickAway(elementRef) // Now will work regardless of passed `ref`.
  return <div ref={combinedRef} />
}

// Click Away will work for both components.
<Component ref={elementRef} />
<Component />

Concurrent

Concurrent happens when you have ref that is coming from a parent but your element also have a ref, you may say

Just use the ref that the parent is passing (-_-)/

This will actually work, but only in cases when the parent is passing an object.
If it's a callback, it won't work that easily, you will have to use useEffect to call passing ref, which is not ideal and may have consequences.

To solve it, you will need to pass a callback to the element and call or assign your refs. That's why there is a library that provides a function for that.

Real-World Example

I used TanStack Virtual to virtualize the list and I used ref to measure element dynamically, but I wanted the components of list to have possibility of click away.

In this case I had ref as a callback though components needed a ref object. It's somewhat close to a fallback case.

interface ItemsProps {
  items: object[]
}

function Items(props: ItemsProps) {
  const elementRef = useRef<HTMLDivElement>(null)

  const scrolling = useContext(scrollingContext)
  const virtual = useVirtualizer({
    count: props.items.length,
    gap: 8,
    getScrollElement: () => scrolling.elementRef.current,
    estimateSize: () => 60,
    measureElement: element => element.scrollHeight,
    overscan: 5
  })

  const items = virtual.getVirtualItems()

  return (
    <div className="items" style={{ height: virtual.getTotalSize(), zIndex: 0 }} ref={elementRef}>
      {items.map(item => (
        <div style={{ height: item.size, transform: `translateY(${item.start}px)`, zIndex: -item.index }} key={item.key}>
          <Item {...props.items[item.index]} index={item.index} ref={virtual.measureElement} />
        </div>
      ))}
    </div>
  )
}


interface ItemProps {
  index: number
  ref?: Ref<HTMLDivElement>
}
function Item(props: ItemProps) {
  const elementRef = useRef<HTMLDivElement>(null)
  const combinedRef = combineRefs([elementRef, props.ref])

  const [expanded, setExpanded] = useState(false)
  useClickAway(elementRef, () => setExpanded(false))

  return <div onClick={() => setExpanded(true)} ref={combinedRef} />
}

The code is simplified and generalized to not disclose any source code

@eps1lon eps1lon changed the title [React 19] Refs merging/combining Refs merging/combining Jun 6, 2024
@monikkacha
Copy link

monikkacha commented Jun 7, 2024

@eps1lon is it ok if i work on this issue and raise PR ?

@eps1lon
Copy link
Contributor

eps1lon commented Jun 7, 2024

Sure, but no promises when this will be reviewed or if it will be merged at all.

@monikkacha
Copy link

That sounds depressing 😅, but I will still work on the feature and raise PR later on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants