61

I have a SideNav component that contains dynamically created links that need to scroll to a corresponding header in an adjacent html table (InfoTable). I've tried multiple different ways of accomplishing this to no avail.

export default class Parent extends Component {
  state = {
    categories: [],
  }

  scrollToHeader = (tableRefString) => {
    // Function to pass to SideNav to access refs in InfoTable
    this[tableRefString].scrollIntoView({ block: 'start' });
  }

  render() {
    return (
      <div>
        <SideNav
          categories={this.state.categories}
          scrollToHeader={this.scrollToHeader} />
        <InfoTable
          categories={this.state.categories} />
      </div>
    );
  }
}

export default class InfoTable extends Component {
  render() {
    return (
      <div>
        <table>
          <tbody>
            {this.props.categories.map(category => (
              <>
                // Forward the ref through InfoTableHeader to be set on the parent DOM node of each InfoTableHeader
                <InfoTableHeader />
                {category.inputs.map(input => <InfoTableRow />)}
              </>
            ))}
          </tbody>
        </table>
      </div>
    );
  }
}

In order to click a link on SideNav and scroll to the corresponding header on InfoTable, I believe that I need to forward refs that are dynamically created on Parent based on names in my categories array and set these refs to the DOM nodes for each header in InfoTable. From there I would pass a function to SideNav that could access the refs in Parent in order to scroll to the header.

  • How can I go about forwarding multiple refs at once to my InfoTable component?
  • Is there a cleaner way to accomplish what I'm trying to do? I've looked into React.findDOMNode() but refs seem to be the better option.
EJ Morgan
  • 722
  • 1
  • 6
  • 8

6 Answers6

78

I know there is an already accepted answer, and while I find @nicholas-haley's solution acceptable. I think a better way to go about it would be to use the built-in useImperativeHandle hook.

IMPORTANT: The React Hooks Api is available as of

  • react@16.8.0 and later
  • react-native@0.59.0 and later

The React hooks API Docs state:

useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with `forwardRef

This note is followed by the following example:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

Thus, in my opinion, a much cleaner solution would be to delegate the needed refs through the useImperativeHandle hook.

This way there is no need for a special ref syntax, and the component can simply return a specific type of a FatherRef; Example:

// LabelInput.js
function LabelInput(props, ref) {
    const labelRef = useRef();
    const inputRef = useRef();

    useImperativeHandle(ref, () => ({
      focus: () => {
       inputRef.current.focus();
      },
      get input() {
          return inputRef.current;
      },
      get label() {
          return labelRef.current;
      },
      // ... whatever else one may need
    }));
    return (
      <div>
        <label ref={labelRef} ... />
        <input ref={inputRef} ... />;
      </div>
    )
}
LabelInput = forwardRef(LabelInput);

function MyScreen() {
   const labelInputRef = useRef();

   const onClick = useCallback(
    () => {
//       labelInputRef.current.focus(); // works
//       labelInputRef.current.input.focus(); // works
// ... etc
    },
    []
   );

   return (
     ...
      <LabelInput ref={labelInputRef} ... />
     ....
   )
}

EDIT: Plus while the old doc is still totally relevant for this usecase, here is also the new link https://react.dev/reference/react/useImperativeHandle#exposing-your-own-imperative-methods

abernier
  • 27,030
  • 20
  • 83
  • 114
Samer Murad
  • 2,479
  • 25
  • 27
  • I'm just curious with this one as I think I'm missing something, but how would you do this with a React Class component? – Glenn Flanagan Aug 31 '21 at 11:20
  • You Can't, unfortunately. Prior to react-hooks you would've had to hack-up a solution like @nicholas-haley's answer. You could, technically, create a reference in the constructor of a component using `React.createRef()` and then manually add methods on it's `this.ref.current`, but that would be very bad, the catastrophic type of bad; and in general, working with refs is not, "The React Way" of doing things, the docs like to mention it each time they get a chance https://reactjs.org/docs/refs-and-the-dom.html. But honestly, it's worth it to refactor class components in the long run anyways – Samer Murad Aug 31 '21 at 12:07
  • The hooks APIs are really powerful, and in my humble opinion, the composition approach is always better than the inheritance approach. Combine that with the fact that hook based component is (at least for now) the direction react has taken, I wouldn't even spend time on trying to solve this ref problem with a class component and really just refactor it – Samer Murad Aug 31 '21 at 12:09
  • for people whom disagree with hooks, `this.childRefs = {current:{ref1:this.ref1}}` in the constructor and `export default React.forwardRef((props, ref) => );` `` – Nick Carducci for Carface Bank Feb 03 '22 at 23:07
62

I had a similar situation where I needed multiple refs to be forwarded to the child of my Parent component.

I still have not found an elegant solution, however you might try passing your refs as an object, and destructuring within the forwardRef callback:

// Parent
ref={{
    ref1: this.ref1,
    ref2: this.ref2
}}

// Child
export default React.forwardRef((props, ref) => {
  const { ref1, ref2 } = ref;

  return (
    <Child1
      {...props}
      ref={ref1}
    />
    <Child2
      {...props}
      ref={ref2}
    />
  );
});

I'm not a big fan of the naming here (I'd prefer ref to be called refs), but this works in case you're in a pinch.

EDIT:

In 2020 I believe @samer-murad's answer is the best solution to this problem.

Nicholas Haley
  • 3,558
  • 3
  • 29
  • 50
  • 7
    I scoured the internet and couldn't end up finding an elegant solution, either. It doesn't seem like there's any kind of established pattern to accomplish this (maybe for a reason?). I ended up just passing the refs down in an object and attaching them right out of the object instead of using React.forwardRef. Everything works fine doing it that way, is there anything I'm missing by not using React.forwardRef? – EJ Morgan Dec 20 '18 at 15:23
  • @EJ Morgan how do you pass a ref as an object and attach them manually? – Pangamma Jan 02 '20 at 23:12
  • @EJMorgan I just noticed that I can even pass a ref to a functional component by just passing it to any other prop name but "ref"... I also feel that I might be missing something here, but it works – Marnix.hoh Dec 20 '20 at 09:20
  • 1
    This approach is not correct as `ref` should be a `callback` or an object which has a `current` key. Try to use that code with TypeScript and this code won't compile. Try to pass two custom ref props e.g., `` – Manvendra SK Sep 08 '21 at 14:17
  • 1
    I wonder why they couldn't just allow `forwardRef((props, ...refs) =>`. – Robo Robok May 30 '22 at 09:50
  • **Question:** How do you make it work with typescript? – Sebastian Nielsen Sep 11 '22 at 17:16
4

I actually just picked this up from react-hook-form, but they presented a nice way to share ref and assign multiple refs at once:

<input name="firstName" ref={(e) => {
  register(e) // use ref for register function
  firstNameRef.current = e // and you can still assign to ref
}} />
amaster
  • 1,915
  • 5
  • 25
  • 51
1

I still don't understand what goes on in that ref property, but the following React source code typeof type.render === 'function' + the other answers led me to try passing a function of hoisted refs from parent, and it works!

class Child extends React.Component {
  render() {
    return (
      <div>
        <div
          ref={this.props.pa}
          style={this.props.style}
          onClick={async () => {
            this.storeAuth(...this.props.storableAuth);
            this.props.clearStore();
          }}
        />
        <div
          ref={this.props.fwd}
          style={this.props.style}
          onClick={async () => {
            this.props.onStart();
            const res = await this.getUserInfo(verbose, auth, storedAuth);
            if (res === "login?") this.props.onPromptToLogin();
            if (res) this.props.onFinish(); //res.isAnonymous
          }}
        />
      </div>
    );
  }
}

export default React.forwardRef((props, getRefs) => {
  const { pa, fwd } = props.getRefs();
  return <Child fwd={fwd} pa={pa} {...props} />;
});


class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.pa = React.createRef();
    this.fwd = React.createRef();
  }
  render() {
    return (
      <Child
        getRefs={() => {
          return {
            pa: this.pa,
            fwd: this.fwd
          };
        }}
        storableAuth={this.state.storableAuth}//[]
        clearAuth={() => this.setState({ storableAuth: null })}
      />
    );
  }
}
  • You can also use the child forwardRef `ref={{current:Object.fromEntries(new Map([“exref”, React.createRef()]))}}`. for dynamic refs the `forwardRef` Child wrapper: `var dynamic1 = []; for (let i = 0; i < 200; i++) { dynamic1.push("scrollImg" + i); } const destruct = (obj, ...keys) => keys.reduce((a, c) => ({ ...a, [c]:obj[c] }), {}); const subset1 = destruct( ref.current, "namedref", …dynamic1 );` after setting in parent constructor `for (let i = 0; i < 200; i++) { genChildRefs2.push(["scrollImg" + i, React.createRef()]);}`, push/spread `genChildRefs2` into `new Map([…])` from entries current. – Nick Carducci for Carface Bank Jul 19 '22 at 04:16
  • Try as we might, these hacked refs have no access to the element's function `click()`/listener `onClick`, yet will work for purposes `window.scroll(0,this.props.name.current.offsetHeight);` – Nick Carducci for Carface Bank Oct 24 '22 at 00:42
0

This is not exactly what the author of this question asked, but this title could feet to this question as well: how do I allow developers using my React component to pass a ref if I pass ref internally in my component also (mean, passing multiple refs that will get a ref for this element),

this is the solution i came with:

import { useState, useRef } from "react";

export default function App() {
  const [, render] = useState({});
  const reRender = () => render({});

  const divRef = useRef();
  console.log("App", divRef);
  return <Component reRender={reRender} passPropsToDiv={{ ref: divRef }} />;
}

const Component = ({ passPropsToDiv, reRender }) => {
  const div1Ref = useRef();
  const { ref: extraRef = null, ...passPropsToDivNoRef } = passPropsToDiv ?? {};
  extraRef.current = div1Ref.current;
  console.log("Component", div1Ref);

  return (
    <div className="App">
      <div ref={div1Ref} {...passPropsToDivNoRef}>
        i will have a ref
      </div>
      <button onClick={reRender}>reRender</button>
    </div>
  );
};

codesandbox: https://codesandbox.io/s/react-use-pass-multiple-refs-legm7p?file=/src/App.js

Eliav Louski
  • 3,593
  • 2
  • 28
  • 52
0

You can pass multiple refs via the forwardRef() function by using the useImperativeHandle() hook. This function allows you to define functions to expose to the parent element, using the second argument of useImperativeHandle().

Here's an example code for your Child component with forwardRef() and useImperativeHandle() to pass the three refs to the parent element:

import React, { forwardRef, useImperativeHandle, useRef } from 'react';

export interface ChildRef {
  containerRef: HTMLDivElement | null;
  segmentsRef: (HTMLDivElement | null)[];
  cursorRef: HTMLDivElement | null;
}

export interface IChildProps {
  // props of your component
}

const Child = forwardRef<ChildRef, IChildProps>((props, ref) => {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const segmentsRef = useRef<(HTMLDivElement | null)[]>([]);
  const cursorRef = useRef<HTMLDivElement | null>(null);

  useImperativeHandle(ref, () => ({
    containerRef: containerRef.current,
    segmentsRef: segmentsRef.current,
    cursorRef: cursorRef.current,
  }));

  // rest of your component code
});

export default Child;

Now in the parent component, you can use useRef() to create a reference to the Child element, and pass this reference to Child via the ref prop.

Here's an example code for your Parent component:

import React, { useRef } from 'react';
import Child, { ChildRef } from './Child';

const Parent = () => {
  const childRef = useRef<ChildRef>(null);

  const handleClick = () => {
    console.log(childRef.current?.containerRef);
    console.log(childRef.current?.segmentsRef);
    console.log(childRef.current?.cursorRef);
  };

  return (
    <div>
      <button onClick={handleClick}>Log refs</button>
      <Child ref={childRef} />
    </div>
  );
};

export default Parent;

I hope this helps!

G Clovs
  • 2,442
  • 3
  • 19
  • 24