javascriptreactjsreact-hooksjsxhigher-order-components

React Refs not working with a dynamically generated component in a higher-order component (HOC)


I'm encountering an issue with React refs that's a bit perplexing. I have a higher-order component (HOC) that dynamically generates child components based on a prop, and I'm trying to use refs to access those child components. However, it seems like the refs aren't being properly set, and I can't figure out why.

Here's a simplified example of my code:

import React, { useRef, useEffect } from 'react';

const DynamicComponent = ({ name }) => {
  // Some component logic here...
  return <div>{name}</div>;
};

const HOC = ({ dynamicComponentProps }) => {
  const dynamicRef = useRef();

  useEffect(() => {
    console.log(dynamicRef.current); // This logs 'null'
    // Some other logic with dynamicRef.current...
  }, [dynamicRef.current]);

  const renderDynamicComponent = () => {
    return dynamicComponentProps.map((props, index) => (
      <DynamicComponent ref={dynamicRef} key={index} {...props} />
    ));
  };

  return <div>{renderDynamicComponent()}</div>;
};

const App = () => {
  const dynamicComponentProps = [
    { name: 'Component A' },
    { name: 'Component B' },
    { name: 'Component C' },
  ];

  return <HOC dynamicComponentProps={dynamicComponentProps} />;
};

In the useEffect block inside the HOC, the dynamicRef.current is always null, even though the components are rendered. Am I missing something about refs in dynamically generated components, or is there a better approach to achieve what I'm trying to do?

What I Tried:

I attempted to use React refs in a higher-order component (HOC) that dynamically generates child components based on a prop. I created a ref (dynamicRef) within the HOC and assigned it to each dynamically generated child component.

What I Expected:

I expected that when I access dynamicRef.current in the useEffect block, it would point to the last dynamically generated child component, allowing me to perform actions or access properties of that component.

What Actually Resulted:

Surprisingly, when I logged dynamicRef.current, it always showed null, indicating that the refs weren't being properly set. Despite the components being rendered, the ref wasn't pointing to any of them.


Solution

  • The problem is that you are trying to assign the same ref for all your components. a ref needs to be assined to a single instance of a component so we can modify your code to the following

    import React, { useRef, useEffect } from "react";
    
    const DynamicComponent = React.forwardRef(({ name }, ref) => {
      // Some component logic here...
      return <div ref={ref}>{name}</div>;
    });
    
    const HOC = ({ dynamicComponentProps }) => {
      const dynamicRefs = dynamicComponentProps.map(() => useRef(null));
    
      useEffect(() => {
        dynamicRefs.forEach((ref) => {
          console.log(ref.current); // This should log the div element
          // Some other logic with ref.current...
        });
      }, [dynamicRefs]);
    
      const renderDynamicComponent = () => {
        return dynamicComponentProps.map((props, index) => (
          <DynamicComponent ref={dynamicRefs[index]} key={index} {...props} />
        ));
      };
    
      return <div>{renderDynamicComponent()}</div>;
    };
    
    const App = () => {
      const dynamicComponentProps = [
        { name: "Component A" },
        { name: "Component B" },
        { name: "Component C" },
      ];
    
      return <HOC dynamicComponentProps={dynamicComponentProps} />;
    };
    
    export default App;
    

    in this updated version we have a seperate ref foreach component. to make this work we add to modfiy a few other things as well

    1. changed DynamicComponent to forwardRef so that we can pass a ref
    2. in the useEffect we log the array of refs
    3. in renderDynamiccomponent we assign a different ref to each component

    EDIT

    As pointed out in the comments the above code does break react rule of calling hook in side a loop so we can change the code a little instead of having an array of refs we now have a ref containings an array

    import React, { useRef, useEffect } from "react";
    
    const DynamicComponent = React.forwardRef(({ name }, ref) => {
      // Some component logic here...
      return <div ref={ref}>{name}</div>;
    });
    
    const HOC = ({ dynamicComponentProps }) => {
      const dynamicRefs = useRef(
        dynamicComponentProps.map(() => ({ current: null }))
      );
    
      useEffect(() => {
        dynamicRefs.current.forEach((ref) => {
          console.log(ref.current); // This should log the div element
          // Some other logic with ref.current...
        });
      }, [dynamicRefs.current]);
    
      const renderDynamicComponent = () => {
        return dynamicComponentProps.map((props, index) => (
          <DynamicComponent
            ref={dynamicRefs.current[index]}
            key={index}
            {...props}
          />
        ));
      };
    
      return <div>{renderDynamicComponent()}</div>;
    };
    
    const App = () => {
      const dynamicComponentProps = [
        { name: "Component A" },
        { name: "Component B" },
        { name: "Component C" },
      ];
    
      return <HOC dynamicComponentProps={dynamicComponentProps} />;
    };
    
    export default App;