Search code examples
javascriptreactjsreact-hooksparent-childreact-ref

Problems using useRef / useImperativeHandle in mapping components


I have a dashboard with different components. Everything is working with a separate start-button on each component, now I need to have a common start-button, and for accessing the children's subfunctions from a parent, I understand that in React you should use the useRef.(but its perhaps not correct, but I'm struggling to see another way). I would like to have the flexibility to choose which component to start from this "overall start-button"

I have a component list that i map through shown below.

return(
{ComponentsList.map((item) => {
      return (
       <Showcomponents
        {...item}
        key={item.name}
       />
)

This works fine, but I would like, as mentioned, to access a function called something like "buttonclick" in each of the children, so I tested this with a pressure-gauge component

The function "exposed" via the forwardRef and the useImparativeHandle

const ShowRadialGauge = forwardRef((props, ref) => {
 useImperativeHandle(ref, () => ({
  buttonclick() {
   setStart(!start);
  },
 }));
)

then in my dashboard I changed to :

const gaugepressure = useRef();

return(
  <div>
    <Button onClick={() => gaugepressure.current.buttonclick()}>
      Start processing
    </Button>
    <ShowRadialGauge ref={gaugepressure} />
  <div>
)

This works fine if I use the useRef from the dashboard and instead of mapping over the components, I add them manually.

I understand the useRef is not a props, but its almost what I want. I want to do something like this:

return(
{ComponentsList.map((item) => {
  return (
    <Showcomponents
      {...item}
      key={item.name}
      **ref={item.ref}**
   />
)

where the ref could be a part of my component array (as below) or a separate array.

export const ComponentsList = [
 {
  name: "Radial gauge",
  text: "showradialgauge",
  component: ShowRadialGauge,
  ref: "gaugepressure",
 },
 {
  name: "Heatmap",
  text: "heatmap",
  component: Heatmap,
  ref: "heatmapstart",
 },
]

Anyone have any suggestions, or perhaps do it another way?


Solution

  • You are on the right track with a React ref in the parent to attach to a single child component. If you are mapping to multiple children though you'll need an array of React refs, one for each mapped child, and in the button handler in the parent you will iterate the array of refs to call the exposed imperative handle from each.

    Example:

    Parent

    // Ref to hold all the component refs
    const gaugesRef = React.useRef([]);
    
    // set the ref's current value to be an array of mapped refs
    // new refs to be created as needed
    gaugesRef.current = componentsList.map(
      (_, i) => gaugesRef.current[i] ?? React.createRef()
    );
    
    const toggleAll = () => {
      // Iterate the array of refs and invoke the exposed handle
      gaugesRef.current.forEach((gauge) => gauge.current.toggleStart());
    };
    
    return (
      <div className="App">
        <button type="button" onClick={toggleAll}>
          Toggle All Gauges
        </button>
        {componentsList.map(({ name, component: Component, ...props }, i) => (
          <Component
            key={name}
            ref={gaugesRef.current[i]}
            name={name}
            {...props}
          />
        ))}
      </div>
    );
    

    Child

    const ShowRadialGauge = React.forwardRef(({ name }, ref) => {
      const [start, setStart] = React.useState(false);
    
      const toggleStart = () => setStart((start) => !start);
    
      React.useImperativeHandle(ref, () => ({
        toggleStart
      }));
    
      return (....);
    });
    

    Edit problems-using-useref-useimperativehandle-in-mapping-components

    The more correct/React way to accomplish this however is to lift the state up to the parent component and pass the state and handlers down to these components.

    Parent

    const [gaugeStarts, setGaugeStarts] = React.useState(
      componentsList.map(() => false)
    );
    
    const toggleAll = () => {
      setGaugeStarts((gaugeStarts) => gaugeStarts.map((start) => !start));
    };
    
    const toggleStart = (index) => {
      setGaugeStarts((gaugeStarts) =>
        gaugeStarts.map((start, i) => (i === index ? !start : start))
      );
    };
    
    return (
      <div className="App">
        <button type="button" onClick={toggleAll}>
          Toggle All Guages
        </button>
        {componentsList.map(({ name, component: Component, ...props },, i) => (
          <Component
            key={name}
            start={gaugeStarts[i]}
            toggleStart={() => toggleStart(i)}
            name={name}
            {...props}
          />
        ))}
      </div>
    );
    

    Child

    const ShowRadialGauge = ({ name, start, toggleStart }) => {
      return (
        <>
          ...
          <button type="button" onClick={toggleStart}>
            Toggle Start
          </button>
        </>
      );
    };
    

    Edit problems-using-useref-useimperativehandle-in-mapping-components (forked)