Search code examples
reactjsreact-hooksparent-childuse-refreact-forwardref

useImperativeHandle usage for children componenent but cannot get function from parent


I have app writen by react:

the main App is:

function App({articles}) {
    ...
    const SortVote=()=>{
      console.log(childRef.current);
      console.log(Object.getOwnPropertyNames(childRef.current))
      childRef.current.SortVote();
    }
  
    const [art, setArt]= useState(articles);
    const childRef = useRef();

    return (
        <div className="App">
            <h8k-navbar header={title}></h8k-navbar>
            <div className="layout-row align-items-center justify-content-center my-20 navigation">
                <label className="form-hint mb-0 text-uppercase font-weight-light">Sort By</label>
                <button data-testid="most-upvoted-link" className="small" onClick={SortVote}>Most Upvoted</button>
            </div>
            <Articles articles={art} cRef={childRef}/>
        </div>
    );

}

the children componenet is:

function Articles({ articles, cRef }) {
  const [arts, setArts] = useState(articles);
  const sortVote = () => {
    const result = articles.sort((a, b) => {
      if (a.upvotes > b.upvotes) {
        return -1;
      }
      if (a.upvotes < b.upvotes) {
        return 1;
      }
      return 0;
    });
    setArts(result);
  };
  const sortDate = () => {
    const result = articles.sort((a, b) => {
      if (a.date > b.date) {
        return -1;
      }
      if (a.date < b.date) {
        return 1;
      }
      return 0;
    });
    setArts(result);
  };
  useImperativeHandle(cRef, () => ({
    sortVote,
    sortDate
  }));
  if (articles)
    return (
      <div className="card w-50 mx-auto">
        <table>
          <thead>
            <tr>
              <th>Title</th>
              <th>Upvotes</th>
              <th>Date</th>
            </tr>
          </thead>
          <tbody>
            {arts.map((item, index) => (
              <tr data-testid="article" key={index}>
                <td data-testid="article-title">{item.title}</td>
                <td data-testid="article-upvotes">{item.upvotes}</td>
                <td data-testid="article-date">{item.date}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    );
}

I use useImperativeHandle in children Component and Try to use 'childRef.current.SortVote()' in parent Component to invoken children Component function. But the browser has error:

childRef.current.SortVote is not a function

The full code is on: https://stackblitz.com/edit/react-vy7df9

At useImpertiveHandle hook, the SortVote and sortDate is already defined function, but why here still error SortVote is not function?


Solution

  • Issue

    The useImperativeHandle is used with passed React refs, not props holding ref values.

    useImperativeHandle

    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:

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

    In your code you are not passing a ref to the child component, you are passing a prop that has a ref value.

    <Articles cRef={childRef} articles={art} />
    

    vs

    <Articles ref={childRef} articles={art} />
    

    Solution

    First, update the function signature of Articles component to consume a React ref. Note that this is a second argument passed to the function, props object being the first.

    function Articles({ articles }, ref) {
      ...
    
      useImperativeHandle(ref, () => ({
        sortVote,
        sortDate
      }));
    
      if (articles) {
        return (
          ...
        );
      }
      return null;
    }
    

    Next, wrap the Articles component in React.forwardRef.

    Articles = forwardRef(Articles);
    

    Finally, pass a ref to the Articles component. Note, it is sortVote, not SortVote.

    function App({articles}) {
      const [art, setArt]= useState(articles);
      const childRef = useRef();
    
      ...
      const SortVote=()=>{
        console.log(childRef.current);
        console.log(Object.getOwnPropertyNames(childRef.current))
        childRef.current.sortVote();
      }
    
      return (
        <div className="App">
          ...
          <Articles
            ref={childRef}
            articles={art}
          />
        </div>
      );
    }
    

    Update

    You've also a bug in the child Articles component. Array.prototype.sort does an in-place sorting, which means it mutates the arts state. You first need to copy the array before sorting it. Array.prototype.slice is a simple functional programming way to copy the array inline before sorting it.

    function Articles({ articles }, ref) {
      const [arts, setArts] = useState(articles);
    
      const sortVote = () => {
        const result = articles.slice().sort((a, b) => {
          if (a.upvotes > b.upvotes) {
            return -1;
          }
          if (a.upvotes < b.upvotes) {
            return 1;
          }
          return 0;
        });
        setArts(result);
      };
    
      const sortDate = () => {
        const result = articles.slice().sort((a, b) => {
          if (a.date > b.date) {
            return -1;
          }
          if (a.date < b.date) {
            return 1;
          }
          return 0;
        });
        setArts(result);
      };
    
      ...