Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I pass ref to a neighbour component

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;
like image 541
LazioTibijczyk Avatar asked Oct 14 '22 23:10

LazioTibijczyk


1 Answers

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.

like image 141
Drew Reese Avatar answered Oct 31 '22 20:10

Drew Reese