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.FCare all built-in type declarations."
FC= FunctionComponent.<Props>= generic parameter, telling TypeScript "this component's props must satisfy the definition of the Props interface".
2. React.FC and Generics
2.1 Understanding from the source code level
The teacher said:
"
type FC<P = {}> = FunctionComponent<P>— React source code.FunctionComponentfunction component class declaration, the return must beReactElement.typetype alias,FCis 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
editingNameis 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:
- Child component is reusable — the same input box component can be used anywhere text editing is needed.
- Better performance — no state of its own, won't trigger unnecessary re-renders.
- Clear data flow — all state is in the parent component, modification path is single.
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
editingNamein 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.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
💪💪💪