🚩 React + TypeScript 的 Props 通信,我从"把事件对象传给父组件"进化到了"只传值"

写在前面:今天学了一个非常"实用"但又有点抽象的知识点------React + TypeScript 中父子组件如何优雅地通信 。老师拿了一个"修改用户名"的小功能,前后写了三个版本。从最原始的"把整个事件对象传给父组件",到"子组件自己管理输入状态",再到"状态提升到父组件但子组件无状态"------三次迭代,我一开始觉得"不都一样吗?"看完之后才发现,差距太大了。TypeScript 的强类型约束 + React 的单向数据流,写出来的代码可读性和健壮性完全不是一个量级。


一、TypeScript 给 React 带来了什么?

1.1 为什么要用 TypeScript?

老师说:

"React + TS 非常适合企业级开发。TS 提供了类型约束、静态编译、大型语言的丰富功能。"

纯 JS 写 React:

jsx 复制代码
const Hello = (props) => {
    return <h2>Hello {props.userName}</h2>
};

用 TS 写 React:

tsx 复制代码
import * as React from 'react';

interface Props {
    userName: string;
}

const Hello: React.FC<Props> = (props) => {
    return <h2>Hello {props.userName}</h2>;
};

区别在哪?

对比 纯 JS TypeScript
props 传错了 运行时才能发现 编译时直接报错
少传了属性 显示 undefined ❌ 编译不过
类型约束 interface Props 强制约束
代码提示 没有 编辑器自动提示 props 字段

React.FC<Props> 的含义:

老师说:

"React.FC------React 函数组件类型。() => ReactNode。React 本身就是用 TS 写的,ReactNodeReact.FC 都是内置的类型声明。"

  • FC = FunctionComponent(函数组件)。
  • <Props> = 泛型参数,告诉 TypeScript "这个组件的 props 必须满足 Props 接口的定义"。

二、React.FC 和泛型

2.1 源码层面理解

老师说:

"type FC<P = {}> = FunctionComponent<P>------React 源码。FunctionComponent 函数组件类的声明,返回一定是 ReactElementtype 类型别名,FC 简短一些。type FC<P = {}>------默认值为 {},如果你传呢?用传递的类型参数来约束。"

typescript 复制代码
// 不传泛型:props 默认是空对象
const Hello: React.FC = (props) => { };

// 传泛型:props 必须满足 Props 接口
interface Props {
    userName: string;
}
const Hello: React.FC<Props> = (props) => {
    return <h2>Hello {props.userName}</h2>;
};

如果父组件这么调用:

tsx 复制代码
// ❌ 错误:Props 要求 userName 是 string,但没传
<Hello />

// ✅ 正确:
<Hello userName="张三" />

// ❌ 错误:Props 没有 age 属性
<Hello userName="张三" age={18} />

TypeScript 在编译时就会拦截这些错误------不会等到运行时才发现。


三、版本一:把事件对象传给父组件(不优雅)

3.1 代码

NameEditComponent.tsx 中注释的部分:

tsx 复制代码
interface Props {
    userName: string;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

const NameEditComponent: React.FC<Props> = (props) => {
    return (
        <div>
            <label>Update Name:</label>
            <input
                value={props.userName}
                onChange={props.onChange}
            />
        </div>
    );
};

父组件 App 中:

tsx 复制代码
const App = () => {
    const [username, setUsername] = React.useState('initialName');

    const setUsernameState = (event: React.ChangeEvent<HTMLInputElement>) => {
        setUsername(event.target.value);
    };

    return (
        <NameEditComponent
            userName={username}
            onChange={setUsernameState}
        />
    );
};

3.2 问题出在哪?

老师说:

"把子组件的 event 对象传给父组件,导致两边都要 React.ChangeEvent<HTMLInputElement>------单向数据流、父子组件通信的 state 交给父组件,props 传给子组件们,应用状态正确的前提(法律?)。"

这个版本有两个问题:

问题 说明
父组件被事件对象污染 父组件需要知道 ChangeEvent<HTMLInputElement> 这个类型
耦合性高 如果输入框换成别的组件(比如下拉框),父组件也要改

父组件本应只关心"值",但现在它不得不关心"事件对象"。


四、版本二:子组件自己管状态(好一些)

4.1 代码

NameEditComponent2.tsx

tsx 复制代码
interface Props {
    initialUserName: string;
    onNameUpdated: (newName: string) => void;
}

const NameEditComponent: React.FC<Props> = (props) => {
    // 自有状态------子组件自己管输入
    const [editingName, setEditingName] = React.useState(
        props.initialUserName
    );

    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setEditingName(e.target.value);
    };

    const onNameSubmit = () => {
        props.onNameUpdated(editingName);  // 只传值,不传事件对象
    };

    return (
        <>
            <label>Update name:</label>
            <input value={editingName} onChange={onChange} />
            <button onClick={onNameSubmit}>Change</button>
        </>
    );
};

这个版本的变化:

变化 之前 现在
子组件 无状态,只接收 props 有自己的 editingName 状态
通信方式 传事件对象 点按钮时才传值 onNameUpdated(editingName)
父组件接口 <input> 的事件类型 只接收 string 值

父组件现在只需要:

tsx 复制代码
<NameEditComponent
    initialUserName={username}
    onNameUpdated={setUsername}
/>

父组件不需要知道 ChangeEvent 是什么,它只接受一个 string 子组件内部的输入逻辑完全私有化。

老师说:

"子组件中添加了私有状态 editingName,onChange 自己修改。提交父组件时只需要给值就好。"


五、版本三:状态提升 + 无状态子组件(性能最优)

5.1 为什么还要升级?

老师说:

"将私有状态提升到父组件,通过 props 传过来,onChange 修改 editingName。子组件没有状态,性能会更好,就负责展示。UI = fn(props)------子组件职责非常单一,就是负责显示。"

版本二的问题:子组件自己有状态,但父组件有时也需要知道"当前输入了什么"------比如想禁用"提交"按钮(当输入为空或和原来一样时)。

5.2 最终版代码

NameEditComponent.tsx(非注释版本):

tsx 复制代码
interface Props {
    editingName: string;
    onNameUpdated: () => void;
    onEditingNameUpdated: (editingName: string) => void;
    disabled: boolean;
}

const NameEdiningComponent: React.FC<Props> = (props) => {
    const {
        editingName,
        onEditingNameUpdated,
        onNameUpdated,
        disabled,
    } = props;

    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        onEditingNameUpdated(e.target.value); // 报告父组件
    };

    const onNameSubmit = () => {
        onNameUpdated();
    };

    return (
        <>
            <label>Update Name:</label>
            <input value={editingName} onChange={onChange} />
            <button disabled={disabled} onClick={onNameSubmit}>Change</button>
        </>
    );
};

父组件 App.tsx

tsx 复制代码
const App = () => {
    const [name, setName] = React.useState<string>('defaultUserName');
    const [editingName, setEditingName] = React.useState('defaultUserName');

    const loadUsername = () => {
        setTimeout(() => {
            setName('name from async call');
            setEditingName('name from async call');
        }, 2000);
    };

    React.useEffect(() => {
        loadUsername(); // 挂载后异步加载用户名
    }, []);

    const setUserNameState = () => {
        setName(editingName);
    };

    return (
        <>
            名字:{name}
            <HelloComponent userName={editingName} />
            <NameEditComponent
                editingName={editingName}
                onNameUpdated={setUserNameState}
                onEditingNameUpdated={setEditingName}
                disabled={editingName === "" || editingName === name}
            />
        </>
    );
};

5.3 这个版本做了什么?

功能 谁管的 怎么实现的
显示当前名字 App(父组件) HelloComponent 展示 name
输入框的值 App 通过 Props 传的 editingName 是父组件的状态
输入框变化时 子组件通过事件上报 onEditingNameUpdated(e.target.value)
提交时 子组件通知父组件 onNameUpdated()
按钮禁用 父组件算好传下来 `disabled={editingName === ""

子组件完全没有自己的状态------它所有的数据都是从 Props 来的,所有的操作都是通过事件上报的。

老师说:

"UI = fn(props)------子组件没有状态,性能会更好,就负责展示。子组件职责非常单一,就是负责显示。"

这个模式的好处:

  • 子组件可以复用------同样的输入框组件可以用在任何需要编辑文本的地方。
  • 性能更好------没有自己的状态,不会触发不必要的重新渲染。
  • 数据流清晰------所有状态都在父组件,修改路径单一。

六、useEffect:异步加载初始数据

6.1 为什么需要 useEffect?

看 App.tsx 中的代码:

tsx 复制代码
React.useEffect(() => {
    loadUsername(); // 组件挂载后,异步加载用户名
}, []);

老师说:

"useEffect------副作用。在组件挂载(mounted)后,再去请求接口,拿到数据,响应式更新。满足组件即刻挂载,快(第一步),更新状态(第二步)。"

useEffect 让组件先快速显示出来(第一步),再去后台加载数据(第二步)。 这是 React 性能优化的核心思想------用户不用等数据加载完才看到页面。

[] 作为依赖数组,表示"只在组件挂载时执行一次",不会在每次更新时重复执行。


七、三个版本的进化对比

版本 子组件状态 通信方式 父组件复杂度 适用场景
V1 无状态 传事件对象 高(要处理 ChangeEvent) 快速实现,不推荐
V2 有私有状态 提交时传值 低(只接收 string) 简单表单输入
V3 无状态 所有事件上报 中(状态在父组件) 需要父组件控制的复杂表单

老师说:

"版本的变迁:① 把子组件的 event 对象传给父组件 → 影响父组件的可读性。② 子组件中添加私有状态 editingName,提交父组件时只需要给值。③ 将私有状态提升到父组件,子组件没有状态,性能会更好,就负责展示。UI = fn(props)。"


八、总结:React + TS 的最佳实践

概念 说明
React.FC 函数组件类型,泛型参数约束 props
interface Props 定义组件需要的属性和方法
单向数据流 父组件持有状态,通过 Props 传给子组件
自定义事件 子组件通过调用父组件传的函数来"上报"
状态提升 多个子组件共享的状态放在共同父组件
无状态子组件 UI = fn(props),性能更好,更易复用
React.ChangeEvent React 合成事件的类型,泛型指定元素类型
useEffect 副作用,挂载后异步加载数据

React + TypeScript 的核心原则:用 interface 约束数据,用单向数据流保证可预测性,用状态提升实现共享。Version 3 是最佳实践------子组件无状态,父组件统一管理,数据流清晰透明。


写在最后

今天这个知识点有点抽象------三个版本的代码看起来都能跑,但可维护性天差地别。以前我写 React 组件,event 对象到处传,从来没考虑过"父组件要不要知道 ChangeEvent 是什么"。现在知道了:父组件应该只关心"值",不应该关心"事件"。

下次面试官问你:"React 父子组件怎么传事件?"

你可以淡定地说:

"最优雅的方式是遵循'子组件无状态,所有操作通过自定义事件上报 '的原则。子组件通过 Props 接收父组件传下来的数据和事件处理函数,内部不管理状态(纯展示组件)。输入变化时子组件调用 onEditingNameUpdated(value) 把值上报,提交时调用 onNameUpdated()。父组件统一管理状态,通过 disabled 等属性控制子组件的行为。这样父组件不需要关心 ChangeEvent 的类型,子组件完全可复用。加上 TypeScript 的 interface Props 约束,数据流清晰、类型安全。如果需要在组件挂载后异步加载数据,用 useEffect 实现,让组件先快速显示再更新数据。"

然后看着面试官满意的表情,心里默念:这波,又稳了。


本文所有代码示例均来自课堂学习资料,真实可运行。

相关推荐
渣波1 小时前
赋予 React “多线程”:Hooks + Web Worker 并发计算架构深度解析
前端·javascript
光影少年1 小时前
Codex 斜杠指令体系与业务场景调度实战
前端·react.js·reactivex
90后的晨仔1 小时前
uni-app中如何跳转?
前端
Y3815326622 小时前
SERP API 请求体 Gzip 压缩优化实战:大数据量降带宽 80%
开发语言·前端·php
踩着两条虫3 小时前
VTJ.PRO AI Agent 技术白皮书
前端·vue.js·低代码
子兮曰3 小时前
解剖 Claude Code:从入口架构逆向工程看 AI 编程工具的信任边界设计
前端·后端·claude
先吃饱再说3 小时前
层层传递太“痛”了:useContext 给 React 组件装了个“无线遥控器”
前端·react.js·前端框架
颜进强3 小时前
前端看后端 15:什么是 DNS?
前端·后端·ai编程
文艺理科生3 小时前
LangChain.js-v1-记忆管理最佳实践
前端·javascript·后端