跪拜 Guibai
← Back to the summary

Stop Passing React Event Objects to Parent Components

Written in front: Today I learned a very "practical" yet somewhat abstract knowledge point — how parent and child components communicate elegantly in React + TypeScript. The teacher used a small "modify username" feature and wrote three versions. From the most primitive "passing the entire event object to the parent component", to "the child component manages its own input state", to "state lifted to the parent component but the child component is stateless" — three iterations. At first I thought "aren't they all the same?" After reading, I realized the gap is huge. The strong type constraints of TypeScript + React's unidirectional data flow produce code whose readability and robustness are on a completely different level.


1. What does TypeScript bring to React?

1.1 Why use TypeScript?

The teacher said:

"React + TS is very suitable for enterprise-level development. TS provides type constraints, static compilation, and the rich features of a large language."

Writing React in pure JS:

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

Writing React with TS:

import * as React from 'react';

interface Props {
    userName: string;
}

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

What's the difference?

Comparison Pure JS TypeScript
Props passed incorrectly Only discovered at runtime Error reported directly at compile time
Missing property Displays undefined ❌ Won't compile
Type constraints None interface Props enforces constraints
Code hints None Editor automatically prompts for props fields

The meaning of React.FC<Props>:

The teacher said:

"React.FC — React Function Component type. () => ReactNode. React itself is written in TS, ReactNode, React.FC are all built-in type declarations."


2. React.FC and Generics

2.1 Understanding from the source code level

The teacher said:

"type FC<P = {}> = FunctionComponent<P> — React source code. FunctionComponent function component class declaration, the return must be ReactElement. type type alias, FC is shorter. type FC<P = {}> — default value is {}, what if you pass something? It uses the passed type parameter to constrain."

// Not passing a generic: props defaults to an empty object
const Hello: React.FC = (props) => { };

// Passing a generic: props must satisfy the Props interface
interface Props {
    userName: string;
}
const Hello: React.FC<Props> = (props) => {
    return <h2>Hello {props.userName}</h2>;
};

If the parent component calls it like this:

// ❌ Error: Props requires userName to be a string, but it wasn't passed
<Hello />

// ✅ Correct:
<Hello userName="Zhang San" />

// ❌ Error: Props has no age property
<Hello userName="Zhang San" age={18} />

TypeScript intercepts these errors at compile time — it won't wait until runtime to discover them.


3. Version 1: Passing the event object to the parent component (not elegant)

3.1 Code

Look at the commented part in NameEditComponent.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>
    );
};

In the parent component App:

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 What's the problem?

The teacher said:

"Passing the child component's event object to the parent component causes both sides to need React.ChangeEvent<HTMLInputElement> — unidirectional data flow, the state for parent-child component communication is given to the parent component, props are passed to child components, the prerequisite for correct application state (law?)."

This version has two problems:

Problem Explanation
Parent component is polluted by the event object The parent component needs to know the ChangeEvent<HTMLInputElement> type
High coupling If the input box is replaced with another component (like a dropdown), the parent component also needs to change

The parent component should only care about the "value", but now it has to care about the "event object".


4. Version 2: Child component manages its own state (better)

4.1 Code

Look at NameEditComponent2.tsx:

interface Props {
    initialUserName: string;
    onNameUpdated: (newName: string) => void;
}

const NameEditComponent: React.FC<Props> = (props) => {
    // Own state — child component manages input itself
    const [editingName, setEditingName] = React.useState(
        props.initialUserName
    );

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

    const onNameSubmit = () => {
        props.onNameUpdated(editingName);  // Only passes the value, not the event object
    };

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

Changes in this version:

Change Before Now
Child component Stateless, only receives props Has its own editingName state
Communication method Passes event object Only passes value on button click onNameUpdated(editingName)
Parent component interface <input> event type Only receives a string value

The parent component now only needs:

<NameEditComponent
    initialUserName={username}
    onNameUpdated={setUsername}
/>

The parent component doesn't need to know what ChangeEvent is, it only accepts a string. The child component's internal input logic is completely private.

The teacher said:

"A private state editingName is added to the child component, onChange modifies it itself. When submitting to the parent component, it only needs to give the value."


5. Version 3: State lifting + stateless child component (optimal performance)

5.1 Why upgrade further?

The teacher said:

"Lift the private state to the parent component, pass it down through props, onChange modifies editingName. The child component has no state, performance will be better, it just handles display. UI = fn(props) — the child component's responsibility is very single, just responsible for displaying."

Version 2's problem: The child component has its own state, but the parent component sometimes also needs to know "what is currently being typed" — for example, wanting to disable the "submit" button (when the input is empty or the same as before).

5.2 Final version code

Look at NameEditComponent.tsx (non-commented version):

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); // Reports to parent component
    };

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

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

Parent component App.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(); // Async load username after mount
    }, []);

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

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

5.3 What does this version do?

Feature Who manages it How it's implemented
Display current name App (parent component) HelloComponent displays name
Input box value App passes via Props editingName is the parent component's state
When input changes Child component reports via event onEditingNameUpdated(e.target.value)
On submit Child component notifies parent onNameUpdated()
Button disabled Parent calculates and passes down disabled={editingName === "" || editingName === name}

The child component has absolutely no state of its own — all its data comes from Props, all operations are reported through events.

The teacher said:

"UI = fn(props) — the child component has no state, performance will be better, it just handles display. The child component's responsibility is very single, just responsible for displaying."

Benefits of this pattern:


6. useEffect: Async loading initial data

6.1 Why is useEffect needed?

Look at the code in App.tsx:

React.useEffect(() => {
    loadUsername(); // After component mounts, async load username
}, []);

The teacher said:

"useEffect — side effects. After the component is mounted, then request the API, get the data, reactive update. Satisfies immediate component mounting, fast (first step), update state (second step)."

useEffect allows the component to display quickly first (first step), then load data in the background (second step). This is the core idea of React performance optimization — users don't have to wait for data to load before seeing the page.

[] as the dependency array means "only execute once when the component mounts", it won't repeat execution on every update.


7. Evolution comparison of the three versions

Version Child component state Communication method Parent component complexity Applicable scenario
V1 Stateless Passes event object High (needs to handle ChangeEvent) Quick implementation, not recommended
V2 Has private state Passes value on submit Low (only receives string) Simple form input
V3 Stateless All events reported Medium (state in parent component) Complex forms needing parent control

The teacher said:

"Version changes: ① Passing the child component's event object to the parent component → affects the parent component's readability. ② Adding private state editingName in the child component, only needs to give the value when submitting to the parent component. ③ Lifting private state to the parent component, the child component has no state, performance will be better, it just handles display. UI = fn(props)."


8. Summary: Best practices for React + TS

Concept Explanation
React.FC Function component type, generic parameter constrains props
interface Props Defines the properties and methods the component needs
Unidirectional data flow Parent component holds state, passes it to child components via Props
Custom events Child component "reports" by calling functions passed by the parent component
State lifting State shared by multiple child components is placed in the common parent component
Stateless child component UI = fn(props), better performance, easier to reuse
React.ChangeEvent Type for React synthetic events, generic specifies the element type
useEffect Side effects, async load data after mount

Core principles of React + TypeScript: Use interface to constrain data, use unidirectional data flow to ensure predictability, use state lifting to achieve sharing. Version 3 is the best practice — child component is stateless, parent component manages uniformly, data flow is clear and transparent.


Written at the end

Today's knowledge point is a bit abstract — the code for all three versions seems to run, but maintainability is worlds apart. Before, when I wrote React components, I passed event objects everywhere, never considering "does the parent component need to know what ChangeEvent is". Now I know: The parent component should only care about the "value", not the "event".

Next time an interviewer asks you: "How do React parent and child components pass events?"

You can calmly say:

"The most elegant way is to follow the principle of 'child component is stateless, all operations are reported through custom events'. The child component receives data and event handler functions passed down from the parent component through Props, and does not manage state internally (pure display component). When the input changes, the child component calls onEditingNameUpdated(value) to report the value, and calls onNameUpdated() on submit. The parent component manages state uniformly, controlling the child component's behavior through properties like disabled. This way, the parent component doesn't need to care about the ChangeEvent type, and the child component is completely reusable. With TypeScript's interface Props constraints, the data flow is clear and type-safe. If you need to async load data after the component mounts, use useEffect to implement it, allowing the component to display quickly first and then update data."

Then look at the interviewer's satisfied expression, and silently think: This round, steady again.


All code examples in this article are from classroom learning materials, real and runnable.

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

Darling噜啦啦

💪💪💪