react 父子通讯如何解耦

前言

提到父子组件通讯,我们自然而然的就想到了下一种办法:将需要沟通的状态定义到父组件上,然后在将 状态和设置状态都传入子组件!

import { useState } from 'react';

const HeadForm = (props) => {
  const [motto, setMotto] = useState('');
  return (
    <div className="head-form">
      <div className="form-groups">
        <div className="form-group flex gap-1">
          <label htmlFor="name">姓名</label>
          <input className="border" type="text" value={props.name} onChange={(e) => props.setName(e.target.value)} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">年龄</label>
          <input className="border" type="text" value={props.age} onChange={(e) => props.setAge(e.target.value)} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">签名</label>
          <input className="border" type="text" value={motto} onChange={(e) => setMotto(e.target.value)} />
        </div>
      </div>
    </div>
  );
};

const DemoTwo = () => {
  const [name, setName] = useState('');
  const [age, setAge] = useState('');
  return (
    <div className="wrapper">
      <HeadForm name={name} age={age} setName={setName} setAge={setAge} />
      <div>{name}{age}</div>
    </div>
  );
};

export default DemoTwo;

然而这样我觉得并不好,从设计和落上来看都明明属于子组件的内容,
但是为了便于父子沟通,硬生生把这内容(状态定义)写到了父组件中!
这反而让两个组件有了一定的耦合度,子组件没有那么独立!

我想要的是,子组件是否可以独立运行?哪怕父组件不传递任何参数也能不报错!

方式1:通知父组件

将状态都定义到子组件中,通过监听子组件这些需要传递状态的变化,通知到父组件 props.onChange(searForm)

// 父子组件解耦,父组件实时更新与子组件关联的状态

import { FC, useEffect, useMemo, useState } from 'react';

interface SearForm {
  name: string;
  age: string;
}

interface HeadFormProps {
  onChange: (values: SearForm) => void;
}
const HeadForm: FC<HeadFormProps> = (props) => {
  const [name, setName] = useState('');
  const [age, setAge] = useState('');
  const [motto, setMotto] = useState('');

  // 使用 useMemo 计算一个依赖于 name 和 age 的值(仅收集外部组件所需的值)
  const searForm = useMemo<SearForm>(() => {
    const values = { name, age };
    return values;
  }, [name, age]); // 依赖项

  // 只要searForm变更,就通知父组件更新
  useEffect(() => {
    props.onChange(searForm);
  }, [props, searForm]);

  return (
    <div className="head-form">
      <div className="form-groups">
        <div className="form-group flex gap-1">
          <label htmlFor="name">姓名</label>
          <input className="border" type="text" value={name} onChange={(e) => setName(e.target.value)} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">年龄</label>
          <input className="border" type="text" value={age} onChange={(e) => setAge(e.target.value)} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">签名</label>
          <input className="border" type="text" value={motto} onChange={(e) => setMotto(e.target.value)} />
        </div>
      </div>
    </div>
  );
};

const DemoTwo = () => {
  const [searForm, setSearForm] = useState<SearForm>({ name: '', age: '' });
  const onFormChange = (values) => setSearForm(values);

  const onSubmit = () => {
    console.log(searForm);
  };
  return (
    <div className="wrapper">
      <HeadForm onChange={onFormChange} />
      <div>{JSON.stringify(searForm)}</div>
      <div className="mt-2">
        <button className="bg-blue-200  px-2 rounded cursor-pointer" onClick={onSubmit}>
          提交
        </button>
      </div>
    </div>
  );
};

export default DemoTwo;

上边代码中,我用到了 useMemo来统一收集需要和父组件通信的值,你也可以直接仅用 useEffect

useEffect(() => {
    props.onChange({ name, age });
  }, [props, name, age]);

方式2:通知父组件

方式 2 和方式 1 本质上是一样的,只不过是不在使用 useEffect 或 useMemo来通知,毕竟 useEffect 这个钩子用不好会产生性能问题 (虽然 react 也出了 React Compiler 来优化这些问题)!

下边我是借助于 setXX 来进行设置

import { FC, useState } from 'react';

interface SearForm {
  name: string;
  age: string;
}

interface HeadFormProps {
  onChange: (values: SearForm) => void;
}
const HeadForm: FC<HeadFormProps> = (props) => {
  const [name, _setName] = useState('');
  const [age, _setAge] = useState('');
  const [motto, setMotto] = useState('');

  const setName = (value) => {
    _setName(value);
    props.onChange({ name: value, age });
  };

  const setAge = (value) => {
    _setAge(value);
    props.onChange({ name, age: value });
  };

  return (
    <div className="head-form">
      <div className="form-groups">
        <div className="form-group flex gap-1">
          <label htmlFor="name">姓名</label>
          <input className="border" type="text" value={name} onChange={(e) => setName(e.target.value)} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">年龄</label>
          <input className="border" type="text" value={age} onChange={(e) => setAge(e.target.value)} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">签名</label>
          <input className="border" type="text" value={motto} onChange={(e) => setMotto(e.target.value)} />
        </div>
      </div>
    </div>
  );
};

const DemoTwo = () => {
  const [searForm, setSearForm] = useState<SearForm>({ name: '', age: '' });
  const onFormChange = (values) => setSearForm(values);

  const onSubmit = () => {
    console.log(searForm);
  };
  return (
    <div className="wrapper">
      <HeadForm onChange={onFormChange} />
      <div>{JSON.stringify(searForm)}</div>
      <div className="mt-2">
        <button className="bg-blue-200  px-2 rounded cursor-pointer" onClick={onSubmit}>
          提交
        </button>
      </div>
    </div>
  );
};

export default DemoTwo;

出此之外,我曾经还试图借助于 setXX,直接通知父组件,结果报错:执行setName 的时候,触发组件 立即render,然后在这个过程中 props.onChange 又开始执行,结果就犯了 render 期间禁止更新了状态的禁忌(即React 更新状态应该是纯的,不允许在渲染过程中再次触发其他组件的状态更新,因为这会导致渲染过程变得不可预测)。 以下代码是错误的

const [name, setName] = useState('');
const onNameChange = (e) => {
  setName(() => {
    props.onChange({ name: e.target.value, age });
    return e.target.value;
  });
};

<input className="border" type="text" value={name} onChange={onNameChange} />

方式3:ahooks

使用三方库 ahooks,模拟出 vue 的 v-model.sync的效果

import { FC, useState } from 'react';
import { useControllableValue } from 'ahooks';

interface SearForm {
  name: string;
  age: string;
}

interface HeadFormProps {
  value: SearForm;
  onChange: (values: SearForm) => void;
}
const HeadForm: FC<HeadFormProps> = (props) => {
  const [state, setState] = useControllableValue<SearForm>(props, {
    defaultValue: { // 父组件的参数会覆盖这个默认值
      name: '',
      age: '',
    },
  });
  const [motto, setMotto] = useState('');

  return (
    <div className="head-form">
      <div className="form-groups">
        <div className="form-group flex gap-1">
          <label htmlFor="name">姓名</label>
          <input className="border" type="text" value={state.name} onChange={(e) => setState({...state, name: e.target.value})} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">年龄</label>
          <input className="border" type="text" value={state.age} onChange={(e) => setState({...state, age: e.target.value})} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">签名</label>
          <input className="border" type="text" value={motto} onChange={(e) => setMotto(e.target.value)} />
        </div>
      </div>
    </div>
  );
};

const DemoTwo = () => {
  const [searForm, setSearForm] = useState<SearForm>({ name: '', age: '' });

  const onSubmit = () => {
    console.log(searForm);
  };
  return (
    <div className="wrapper">
      <HeadForm value={searForm} onChange={setSearForm} />
      <div>{JSON.stringify(searForm)}</div>
      <div className="mt-2">
        <button className="bg-blue-200  px-2 rounded cursor-pointer" onClick={onSubmit}>
          提交
        </button>
      </div>
    </div>
  );
};

export default DemoTwo;

如果自己手写,也能实现

import { FC, useEffect, useState } from 'react';

interface SearForm {
  name: string;
  age: string;
}

interface HeadFormProps {
  value: SearForm;
  onChange: (values: SearForm) => void;
}
const HeadForm: FC<HeadFormProps> = (props) => {
  const [state, _setState] = useState<SearForm>({ name: '11', age: '22'});

  const [motto, setMotto] = useState('');

  // 设置子组件状态,要同步到父组件
  const setState = (value: SearForm) => {
    _setState(value);
    if (props.onChange) {
      props.onChange(value);
    }
  };

  // 监听父组件传入值的变更,同步至子组件
  useEffect(() => {
    if (props.value) {
      _setState(props.value);
    }
  }, [props.value]);

  return (
    <div className="head-form">
      <div className="form-groups">
        <div className="form-group flex gap-1">
          <label htmlFor="name">姓名</label>
          <input className="border" type="text" value={state.name} onChange={(e) => setState({ ...state, name: e.target.value })} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">年龄</label>
          <input className="border" type="text" value={state.age} onChange={(e) => setState({ ...state, age: e.target.value })} />
        </div>

        <div className="form-group flex gap-1 mt-2">
          <label htmlFor="name">签名</label>
          <input className="border" type="text" value={motto} onChange={(e) => setMotto(e.target.value)} />
        </div>
      </div>
    </div>
  );
};

const DemoTwo = () => {
  const [searForm, setSearForm] = useState<SearForm>({ name: '33', age: '44' });

  const onSubmit = () => {
    console.log(searForm);
  };
  return (
    <div className="wrapper">
      <HeadForm value={searForm} onChange={setSearForm} />
      <div>{JSON.stringify(searForm)}</div>
      <div className="mt-2">
        <button className="bg-blue-200  px-2 rounded cursor-pointer" onClick={onSubmit}>
          提交
        </button>
      </div>
    </div>
  );
};

export default DemoTwo;

600

posted @ 2025-08-28 16:31  丁少华  阅读(13)  评论(0)    收藏  举报