Search code examples
reactjsrefreact-functional-componentloadable-component

How to pass a ref to a dynamically loaded component


I'd like to pass a ref to a dynamically loaded component. When the component's loaded with a regular import everything works as expected. When using a dynamic import the ref value is undefined || { current: null }.

I get the following the error message in the console:

Warning: Function components cannot be given refs. 
Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

I've attempted to use a React.forwardRef but have been unsuccessful in my attempts.

Parent Component: Recaptcha.tsx

import React from 'react';
import loadable from '@loadable/component';
import ReCAPTCHA from 'react-google-recaptcha';

export type RecaptchaProps = {
  handleOnError: () => void;
  handleOnExpire: () => void;
  forwardedRef: React.MutableRefObject<ReCAPTCHA | null>;
  error: string | null;
  load: boolean;
};

const Recaptcha: React.FunctionComponent<RecaptchaProps> = ({
  handleOnError,
  handleOnExpire,
  error,
  forwardedRef,
  // load,
}) => {
  const RecaptchaPackage = loadable(
    () => import('components/Form/Recaptcha/RecaptchaPackage'),
  );

  return (
    <div>
      <RecaptchaPackage
        handleOnError={handleOnError}
        handleOnExpire={handleOnExpire}
        forwardedRef={forwardedRef}
      />
    </div>
  );
};

export default React.memo(Recaptcha);

Dynamic Component: RecaptchaPackage.tsx

import React from 'react';
import ReCAPTCHA from 'react-google-recaptcha';

export type RecaptchaPackageProps = {
  handleOnError: () => void;
  handleOnExpire: () => void;
  forwardedRef: React.MutableRefObject<ReCAPTCHA | null>;
};

const RecaptchaPackage: React.FunctionComponent<RecaptchaPackageProps> = ({
  handleOnError,
  handleOnExpire,
  forwardedRef,
}) => {
  return process.env.GATSBY_RECAPTCHA_PUBLIC ? (
    <ReCAPTCHA
      onErrored={handleOnError}
      onExpired={handleOnExpire}
      ref={forwardedRef}
      sitekey={process.env.GATSBY_RECAPTCHA_PUBLIC}
      size="invisible"
    />
  ) : null;
};

export default React.memo(RecaptchaPackage);


Solution

  • You're looking to pass a ref of the ReCAPTCHA component (from 'react-google-recaptcha') up through two parent elements. For this you can make use of React's forwardRef.

    Another thing to note is that the ref will not always be assigned a value on component mount, this is true when using with and without dynamic loading. So if you want to log the ref for testing, log in a setTimeout or some other event that is triggered later.

    Your Recaptcha component would look like this:

    import React from "react";
    import loadable from "@loadable/component";
    import ReCAPTCHA from "react-google-recaptcha";
    
    export type RecaptchaProps = {
      handleOnError: () => void;
      handleOnExpire: () => void;
      error: string | null;
      load: boolean;
    };
    
    const RecaptchaPackage = loadable(() => import("./RecaptchaPackage"));
    
    const Recaptcha: React.FunctionComponent<RecaptchaProps> = React.forwardRef(({
      handleOnError,
      handleOnExpire,
      error,
      // load,
    }, ref) => {
      return (
        <div>
          <RecaptchaPackage
            ref={ref}
            handleOnError={handleOnError}
            handleOnExpire={handleOnExpire}
          />
        </div>
      );
    })
    
    export default React.memo(Recaptcha);
    

    And your RecaptchaPackage component would look like this:

    import React from "react";
    
    import ReCAPTCHA from "react-google-recaptcha";
    
    export type RecaptchaPackageProps = {
      handleOnError?: () => void;
      handleOnExpire?: () => void;
    };
    
    const RecaptchaPackage: React.FunctionComponent<RecaptchaPackageProps> = React.forwardRef(({
      handleOnError,
      handleOnExpire
    }, ref) => {
      return process.env.GATSBY_RECAPTCHA_PUBLIC ? (
        <ReCAPTCHA
          ref={ref}
          onErrored={handleOnError}
          onExpired={handleOnExpire}
          sitekey={process.env.GATSBY_RECAPTCHA_PUBLIC}
          size="invisible"
        />
      ) : null);
    });
    
    export default React.memo(RecaptchaPackage);
    

    I've created a sandbox that validates that this should work: https://playcode.io/972680