skip to Main Content

I have a component that handles user authentication and displays a login form if needed, otherwise just displays the children.

function AuthWrapper({children}){
  let [auth, setAuth] = React.useState(null);
  React.useEffect(()=>{
    // Do stuff to check or change state
  }, []);
  let component = (<>Loading...</>);
  if(auth){
    component = (<>{children}</>);
  }
  return component;
}

Then I use it in another component where I need to access stuff in the DOM with a ref.

function App(props) {
  let inputRef = React.useRef();
  React.useEffect(()=>{
    console.log('currentRef', inputRef.current);
  }, []);
  return (
    <AuthWrapper>
      <input placeholder='Enter your name' ref={inputRef} />
    </AuthWrapper>
  );
}

Problem is, obviously, there’s nothing in the DOM to reference at the time the useEffect is called because the AuthWrapper hasn’t rendered the children yet. Therefore, inputRef.current is always undefined.

I kind of understand the problem, but every solution I’ve tried has either failed or just felt really gross and hacky. What are some best practices or clean solutions for setting my reference to a DOM element that doesn’t render immediately, and then accessing it once it does render?

Here’s an online code playground demo: https://playcode.io/1769946

2

Answers


  1. Chosen as BEST ANSWER

    Replacing the useEffect with a useCallback and then using the callback to set the reference was the correct solution, and appears to be best practice when dealing with references in general.

    Something like this...

    const myRef = useRef();
    useEffect(()=>{
      myRef.current.addEventListener('click', myFunct);
      return ()=>myRef.current.removeEventListener('click', myFunct);
    }, [myRef]);
    return <button ref={myRef}>Click me</button>
    

    is better written as

    const myRef = useRef();
    const setMyRef = useCallback(ele=>{
      if(myRef.current){
        // The ref already had a value
        // Do some cleanup here
        myRef.current.removeEventListener('click', myFunct);
      }
      if(ele){
        // a node has been passed, 
        // assign it to the reference 
        // and do whatever I need to with the node
        myRef.current = ele;
        myRef.current.addEventListener('click', myFunct);
      }
    });
    return <button ref={setMyRef}>Click me</button>
    

    The post that cleared it up for me was here.


  2. By default React does not let a component access the DOM nodes of other components. You will have to go through the parent first (in this case that is AuthWrapper). You give the reference(s) to the parent then the parent makes the attachment for you to the specific child(ren). For example:

    function App(props) {
      let inputRef = React.useRef();
      /* ••• */
      return (
        <AuthWrapper ref={inputRef}>
          <input placeholder='Enter your name' />
        </AuthWrapper>
      );
    }

    Now AuthWrapper i.e. the parent is solely responsible for how it lets external routines access it’s child(ren). This design principle is part of a general attempt to have complete component encapsulation. We will now rewrite AuthWrapper as follows:

    const AuthWrapper = React.forwardRef((props, elementRef) => {
      let [auth, setAuth] = React.useState(null);
      /* ••• */
      let component = (<>Loading...</>);
      if(auth){
        component = Children.map(props.children, child => React.cloneElement(child, { ref: elementRef });
      }
      return component;
    }

    If there will be more than one child, you will use multiple refs. I would recommend using useImperativeHandle hook.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search