How do I pass ref to a neighbour component
Asked Answered
T

1

7

I am trying to use ref on a search input in my Header component which ISN'T a higher order component to my ResultsList component. I want to set focus on the Header's search input from the ResultsList component. It is intuitive from the Header because all I have to do is the below. What if I wanted to create a button in ResultsList which would focus on the input element in Header? How do I pass this ref? I have read about forwardRef but I am not passing my ref forwards. ResultsList is not a child of Header.

import React, { useState, useRef } from 'react';
import { useHistory } from 'react-router-dom';

const Header = () => {
  const searchInput = useRef(null);
  const history = useHistory();

  const [searchValue, setSearchValue] = useState(keyword);

  function handleChange(event) {
    setSearchValue(event.target.value);
  }

  function handleSearch(event) {
    event.preventDefault();
    if(searchValue) {
      history.push(`/search/${searchValue}`);
    } else {
      searchInput.current.focus();
    }
  }

  return (
    <form onSubmit={handleSearch} role="search">
      <input
        value={searchValue}
        onChange={handleChange}
        className="HeaderSearch__input"
        id="header-search-input"
        placeholder="Search a repository"
        ref={searchInput}>
      </input>
    </form>
  );
}

export default Header;

My App component looks like this

import React from 'react';
import Header from './Header';
import ResultsList from './ResultsList';

function App() {
  return (
    <>
      <Header />
      <ResultsList />
    </>
  );
}

export default App;
Tippet answered 2/12, 2020 at 20:33 Comment(3)
Look into forwardRef: reactjs.org/docs/forwarding-refs.htmlBranham
@Branham well I did mention that in the description but ResultsList I am adding the button to isn't a child of Header.Tippet
If I got it right, you could do it with a React Context. Please have a look at the following demo. ForwardRef will also work but you'd need to pass it to both components - see demo here. I'd prefer using Context but both is OK.Forehand
C
7

You will need to utilize the "Lifting State Up" pattern. Declare the react ref in App and pass it to both components, to Header to attach the ref to a node and to ResultsList to access the ref and set "focus".

function App() {
  const searchInputRef = useRef(null);
  return (
    <>
      <Header searchInputRef={searchInputRef} />
      <ResultsList searchInputRef={searchInputRef} />
    </>
  );
}

Attach and use the ref as you already are in Header

const Header = ({ searchInputRef }) => {
  const history = useHistory();

  const [searchValue, setSearchValue] = useState(keyword);

  function handleChange(event) {
    setSearchValue(event.target.value);
  }

  function handleSearch(event) {
    event.preventDefault();
    if(searchValue) {
      history.push(`/search/${searchValue}`);
    } else {
      searchInputRef.current.focus();
    }
  }

  return (
    <form onSubmit={handleSearch} role="search">
      <input
        value={searchValue}
        onChange={handleChange}
        className="HeaderSearch__input"
        id="header-search-input"
        placeholder="Search a repository"
        ref={searchInputRef}>
      </input>
    </form>
  );
}

Similarly, you can access searchInputRef in ResultsList component as well.

function ResultsList({ searchInputRef }) {

  ...

  <button
    type="button"
    onClick={() => searchInputRef.current?.focus()}
  >
    Set Search Focus
  </button>
}

Edit

What if more deeply nested components need ref?

If the children components are not direct descendants then you can utilize a react context to allow children to access the ref without needing to pass it as a prop though the React tree.

Create and export the context.

const SearchInputRefContext = React.createContext(null);

Provide the context to children in App

import SearchInputRefContext from '.....';

function App() {
  const searchInputRef = useRef(null);
  return (
    <SearchInputRefContext.Provider value={searchInputRef}>
      <Header />
      <ResultsList />
    </SearchInputRefContext.Provider>
  );
}

Access the context in any sub child component

const Header = () => {
  const history = useHistory();

  const searchInputRef = useContext(SearchInputRefContext);

  const [searchValue, setSearchValue] = useState(keyword);

  function handleChange(event) {
    setSearchValue(event.target.value);
  }

  function handleSearch(event) {
    event.preventDefault();
    if(searchValue) {
      history.push(`/search/${searchValue}`);
    } else {
      searchInputRef.current.focus();
    }
  }

  return (
    <form onSubmit={handleSearch} role="search">
      <input
        value={searchValue}
        onChange={handleChange}
        className="HeaderSearch__input"
        id="header-search-input"
        placeholder="Search a repository"
        ref={searchInputRef}>
      </input>
    </form>
  );
}

No matter how deeply nested

function ReallyDeepComponent() {
  const searchInputRef = useContext(SearchInputRefContext);

  ...

  <button
    type="button"
    onClick={() => searchInputRef.current?.focus()}
  >
    Set Search Focus
  </button>
}

See this section if you happen to still be using class-based components.

Carbonization answered 2/12, 2020 at 22:0 Comment(8)
Yeah, I thought of that but what if my ResultsList is embed deeper down the hierarchy in like ResultsPage > ResultsList? Would I have to pass it down the props twice? Is this a common practice?Tippet
@Tippet It is common to pass props a level down, perhaps a little less common to go another level, or more. This pattern is called "prop drilling" where you pass props many layers deep. The solution react offers is the Context API which "provides a way to pass data through the component tree without having to pass props down manually at every level." You could create a context that provides the shared ref, and the components, so long as they live in the sub-tree of the provider, can access the context value.Carbonization
@Tippet If interested I can update my answer with how I'd do it and try to provide more details about it.Carbonization
it would be great if you could do that. When I first found out what React Context is I thought it's a bit of an overkill to use it for a single ref. Do you think it's reasonable in this case?Tippet
@Tippet Sure thing. I don't find it unreasonable, as it resolves the issue of "prop drilling". I think you'll find the code to be pretty minimal and clean.Carbonization
That's great! One final question - in ReallyDeepComponent you've used question mark after current (searchInputRef.current?.focus()), what does it really mean? I could not find that in the documentation. I was wondering whether this is some sort of typescript's optional property equivalent.Tippet
@Tippet Optional Chaining Operator. It basically is a null check, so searchInputRef.current?.focus() is equivalent to searchInputRef.current && searchInputRef.current.focus().Carbonization
interesting, thanks a lot. I've learned something new today.Tippet

© 2022 - 2024 — McMap. All rights reserved.