6

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;
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
LazioTibijczyk
  • 1,701
  • 21
  • 48
  • Look into `forwardRef`: https://reactjs.org/docs/forwarding-refs.html – SakoBu Dec 02 '20 at 20:37
  • @SakoBu well I did mention that in the description but ResultsList I am adding the button to isn't a child of Header. – LazioTibijczyk Dec 02 '20 at 20:40
  • If I got it right, you could do it with a React Context. Please have a look at the following [demo](https://codesandbox.io/s/react-context-demo-setting-focus-from-other-component-n95h1). `ForwardRef` will also work but you'd need to pass it to both components - see demo [here](https://codesandbox.io/s/react-demo-forwardref-setting-focus-from-other-componet-0sk5u). I'd prefer using Context but both is OK. – AWolf Dec 02 '20 at 21:05

1 Answers1

5

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.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • 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? – LazioTibijczyk Dec 03 '20 at 08:22
  • 1
    @LazioTibijczyk 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](https://reactjs.org/docs/context.html) 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. – Drew Reese Dec 03 '20 at 08:30
  • @LazioTibijczyk If interested I can update my answer with how I'd do it and try to provide more details about it. – Drew Reese Dec 03 '20 at 08:31
  • 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? – LazioTibijczyk Dec 03 '20 at 08:39
  • 1
    @LazioTibijczyk 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. – Drew Reese Dec 03 '20 at 08:41
  • 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. – LazioTibijczyk Dec 03 '20 at 09:13
  • 2
    @LazioTibijczyk [Optional Chaining Operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining). It basically is a null check, so `searchInputRef.current?.focus()` is equivalent to `searchInputRef.current && searchInputRef.current.focus()`. – Drew Reese Dec 03 '20 at 09:22
  • interesting, thanks a lot. I've learned something new today. – LazioTibijczyk Dec 03 '20 at 09:29