Search code examples
reactjsreact-refreact-forwardref

Why does useRef never update current from null?


Problem: cannot get ref to update from {current: null} to the actual ref on the component.

What i want to happen: {current: null}, as i understand it, should update to include the div that ref is on in order to be able to click ouside of it (eventually to close it). 9 understand that it does not update on first render, but it does not ever update. It does run twice on page load, both returning current: null.

What i tried: i have followed all the SO advice to use useEffect and then finally separating it into this function which appears to be the most appropriate and up to date method to do this. It just never updates current.

function useOutsideAlerter(ref) {
  useEffect(() => {

    function handleClickOutside(event) {
      if (ref.current && !ref.current.contains(event.target)) {
        console.log(ref);
      } else {
        console.log("else", ref);
      }
    }
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, []);
}

export const Modal = (props) => {
  const [showModal, setShowModal] = useState(props.showModal);
  const wrapperRef = useRef(null);
  useOutsideAlerter(wrapperRef);

  return (
    <Layout>
      <ModalOuter
        showModal={showModal || props.showModal}
        id={styles["modalOuter"]}
        handleClose={props.handleClose}
      >
        <ModalInner
          ref={wrapperRef}
          handleClose={props.handleClose}
        >
          <Layout display="flex" flexDirection="column">
            <Layout display="flex" flexDirection="column">
              <ModalTitle title={props.title} />
            </Layout>
            <HR />
            <Layout display="flex" flexDirection="column">
              <ModalBody body={props.body} />
            </Layout>
          </Layout>
        </ModalInner>
      </ModalOuter>
    </Layout>
  );
};

ModalInner

    export const ModalInner = (props) => {
  return (
    <Layout
      id={props.id}
      ref={props.ref}
      display="flex"
      justifyContent="center"
      alignItems="center"
      padding="2rem"
      margin="2rem"
      backgroundColor="white"
    >
      {props.children}
    </Layout>
  );
};

Layout Component

export const Layout = (props) => {
  return (
    <div
      id={props.id}
      ref={props.ref}
...

Solution

  • Issue

    In React, there are a few special "props", ref and key are a couple of them. I put quotes around props because while they are passed as props, they are not passed on to or accessible on the props object in children components.

    Solution

    Use React.forwardRef to forward any passed React refs to functional components and expose them in children components.

    export const ModalInner = React.forwardRef((props, ref) => { // <-- access ref
      return (
        <Layout
          id={props.id}
          ref={ref} // <-- pass ref *
          display="flex"
          justifyContent="center"
          alignItems="center"
          padding="2rem"
          margin="2rem"
          borderRadius="5px"
          backgroundColor="white"
          border={`1px solid ${Color.LightGray}`}
          boxShadow={`0rem 0rem 1rem white`}
        >
          {props.children}
        </Layout>
      );
    });
    

    * Note: The Layout and children components will similarly need to forward the ref until you get to where it's actually attached to a DOMNode.

    An alternative solution is to pass the ref as a normal prop.

    <ModalInner
      wrapperRef={wrapperRef}
      handleClose={props.handleClose}
    >
    
    ...
    
    export const ModalInner = (props) => {
      return (
        <Layout
          id={props.id}
          wrapperRef={props. wrapperRef} // <-- pass wrapperRef prop
          display="flex"
          justifyContent="center"
          alignItems="center"
          padding="2rem"
          margin="2rem"
          borderRadius="5px"
          backgroundColor="white"
          border={`1px solid ${Color.LightGray}`}
          boxShadow={`0rem 0rem 1rem white`}
        >
          {props.children}
        </Layout>
      );
    };
    

    Similarly, you need to drill the wrapperRef prop on through to children until you get to the actual DOMNode where you attach the ref.

    Example

    <div ref={props.wrapperRef> .... </div>
    

    You may also find Refs and the DOM docs useful for working with React refs.