[react] 在循环中获取子组件ref,父组件调用子组件的两种方法

2,739 阅读1分钟

在 react 实际项目开发中,有时会需要在父组件中调用子组件方法,而且子组件是在循环中生成的,虽然这种做法不符合 react 的设计哲学,但是在开发中确实有这方面的需求,下面记录一下两种解决方法:

第一种方法是使用useRef/useImperativeHandle/forwardRef:

  1. useRef 初始化为数组
  2. 子组件循环生成中设置 childRefs.current[item.id] = ref;
  3. 子组件需要用forwardRef包一下,(使用connect包裹的子组件需要添加{ forwardRef: true },见这里)
  4. 使用useImperativeHandle 导出子组件的方法,注意子组件的第二个参数

下面是最简化的代码和相关注释:

// 父组件:
import React, { useRef } from 'react';
import ChildComp from './ChildComp';
function App() {
  const arr = [
    {
      id: 1,
      name: '🍎',
    },
    {
      id: 2,
      name: '🍌',
    },
    {
      id: 3,
      name: '🍊',
    },
  ];
  // useRef 初始化为数组
  const childRefs = useRef<any>([]);
  return (
    <div className="App">
      {arr.map((item) => (
        <ChildComp
          // ⚠️注意 ref 有可能为空 所以在此处加一个判断
          ref={(ref) => {
            if (ref) {
              childRefs.current[item.id] = ref;
            }
          }}
          data={item}
          key={item.id}
        />
      ))}
      <button
        onClick={() => {
          childRefs.current &&
            childRefs.current.forEach((childRef: any) => {
              // 调用所有子组件的方法
              childRef.exportChildMethod();
              // 获取所有子组件的值
              console.log(childRef.exportChildValue());
            });
          // 获取某个子组件的值
          console.log(
            childRefs.current && childRefs.current[1].exportChildValue()
          );
        }}
      >
        调用子组件的方法
      </button>
    </div>
  );
}
export default App;

// 子组件
import React, { forwardRef, useState, useImperativeHandle } from 'react';
// 使用forwardRef包裹的组件可以获得一个ref的参数
const Child = (props: any, ref: any) => {
  const { data } = props;
  const [childNum, setChildNum] = useState<number>(0);
  // 在useImperativeHandle中导出被父组件使用的方法
  useImperativeHandle(ref, () => ({
    exportChildMethod: () => {
      childMethod();
    },
    exportChildValue: () => {
      return childNum;
    },
  }));
  const childMethod = () => {
    console.log(`log from child--${data.name}`);
  };
  return (
    <div>
      <p>{data.name}</p>
      <p>{childNum}</p>
      <button
        onClick={() => {
          setChildNum(childNum + 1);
        }}
      >
        child add
      </button>
    </div>
  );
};
// 需要使用forwardRef包裹
export default forwardRef(Child);

第二种方法是不使用 useRef/forwardRef/useImperativeHandle, 原理就是 props 改变触发子组件的方法:

  1. 父组件设置一个毫秒数ms为空
  2. 父组件触发动作,设置毫秒数msnew Date().getTime()
  3. ms传到子组件中
  4. 子组件在useEffect中依赖 ms的值,ms改变触发子组件内的函数
import React from 'react';
import ChildComp from './ChildComp';
function App() {
  const arr = [
    {
      id: 1,
      name: '🍎',
    },
    {
      id: 2,
      name: '🍌',
    },
    {
      id: 3,
      name: '🍊',
    },
  ];
  const [ms, setMs] = useState<number | undefined>();
  const emitChildMethod = () => {
    setMs(new Date().getTime());
  };
  return (
    <div className="App">
      {arr.map((item) => (
        <ChildComp ms={ms} data={item} key={item.id} />
      ))}
      <div>
        <button onClick={emitChildMethod}>调用子组件的方法</button>
      </div>
    </div>
  );
}
export default App;

// 子组件
import React, { useState, useEffect } from 'react';
const Child = (props: any) => {
  const { data } = props;
  const [childNum, setChildNum] = useState<number>(0);
  useEffect(() => {
    // 此处需要做一下判断
    if (ms) {
      childMethod();
    }
  }, [ms]);
  const childMethod = () => {
    console.log(`log from child--${data.name}`);
  };
  return (
    <div>
      <p>{data.name}</p>
      <p>{childNum}</p>
      <button
        onClick={() => {
          setChildNum(childNum + 1);
        }}
      >
        child add
      </button>
    </div>
  );
};
export default Child;

总结一下

  1. 这两种方法都是不得已而为止的办法,破坏了单项数据流的设计模式
  2. 也不利于代码调试和业务梳理
  3. 但是在老代码的改造和添加新功能方面有一定优势