Search code examples
javascriptreactjsref

How to assign a Ref to a button in a React component if I don't own that code? I want to focus it


I'm working on React. There's another team which exposes us a component which has a button in it. It is a dependency for our repository. I want that button to be focused when a particular action is done. I want to do it using refs or any other acceptable way.

myCode.js

focusExternalButton() {
  // some code that focuses the button in the external component
}

render() {

  return {
    <div>
      <ExternalButtonComponent/>
      <button onClick={this.focusExternalButton}>submit</button>
    </div>
  }
} 

ExternalButtonComponent.js

render() {
  return <div><button id="btn-external">This is a button</button></div>
}

How do I put focus on the external button when I click my button? I'm thinking of refs but I'm not sure how I can achieve it.


Solution

  • It surprises me a little that the external component uses ids as rendering it more than once in a page will lead to duplicate ids on the page which isn't valid HTML.

    Nonetheless, yes, you can query the DOM directly, though I would probably do this once on mount and store the result returned element in a ref.

    const { useEffect, useRef } = React;
    
    function App() {
      const buttonRef = useRef();
    
      useEffect(() => {
        buttonRef.current = document.getElementById('btn-external');
      }, []);
    
      function focusExternalButton() {
        buttonRef.current.focus();
      };
    
      return (
        <div>
          <ExternalButtonComponent label="This is a button" />
          <button type='button' onClick={focusExternalButton}>submit</button>
        </div>
      );
    }
    
    function ExternalButtonComponent({label}) {
      return <div><button id="btn-external">{label}</button></div>;
    }
    
    ReactDOM.render(
      <App />,
      document.getElementById("root")
    );
    #btn-external:focus { box-shadow: 0 0 0 3px rgba(21, 156, 228, 0.4);}
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    
    <div id="root"></div>

    However, to avoid having to do this every time you use the component you can wrap the external component and forward the ref.

    const WrappedExternalButtonComponent = forwardRef((props, ref) => {
      useEffect(() => {
        ref.current = document.getElementById('btn-external');
      }, []);
    
      return <ExternalButtonComponent {...props} />
    });
    

    const { useEffect, useRef, forwardRef } = React;
    
    const WrappedExternalButtonComponent = forwardRef((props, ref) => {
      useEffect(() => {
        ref.current = document.getElementById('btn-external');
      }, []);
    
      return <ExternalButtonComponent {...props} />
    });
    
    function ExternalButtonComponent({label}) {
      return <div><button id="btn-external">{label}</button></div>;
    }
    
    function App() {
      const buttonRef = useRef();
    
      function focusExternalButton() {
        buttonRef.current.focus();
      };
    
      return (
        <div>
          <WrappedExternalButtonComponent ref={buttonRef} label="This is a button" />
          <button type='button' onClick={focusExternalButton}>submit</button>
        </div>
      );
    }
    
    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );
    #btn-external:focus { box-shadow: 0 0 0 3px rgba(21, 156, 228, 0.4);}
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    
    <div id="root"></div>

    Or even generalize it further by creating a utility that takes a Component and a query function as arguments and returns a wrapped component.

    const wrapExternalComponent = (Component, query) => forwardRef(({ children, ...props }, ref) => {
      useEffect(() => {
        ref.current = query();
      }, []);
    
      return (
        <Component {...props}>
          {children}
        </Component>
      );
    });
    
    const WrappedExternalButtonComponent = 
      wrapExternalComponent(ExternalButtonComponent, () => document.getElementById('btn-external'));
    

    const { useEffect, useRef, forwardRef } = React;
    
    const wrapExternalComponent = (Component, query) => forwardRef(({ children, ...props }, ref) => {
      useEffect(() => {
        ref.current = query();
      }, []);
    
      return (
        <Component {...props}>
          {children}
        </Component>
      );
    });
    
    function ExternalButtonComponent({label}) {
      return <div><button id="btn-external">{label}</button></div>;
    }
    
    const WrappedExternalButtonComponent = 
      wrapExternalComponent(ExternalButtonComponent, () => document.getElementById('btn-external'));
    
    function App() {
      const buttonRef = useRef();
    
      function focusExternalButton() {
        buttonRef.current.focus();
      };
    
      return (
        <div>
          <WrappedExternalButtonComponent ref={buttonRef} label="This is a button" />
          <button type='button' onClick={focusExternalButton}>submit</button>
        </div>
      );
    }
    
    
    
    
    
    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );
    #btn-external:focus { box-shadow: 0 0 0 3px rgba(21, 156, 228, 0.4);}
    <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
    
    <div id="root"></div>