Parent Component
import { useState } from 'react'
import UserEditor from './UserEditor'
interface User {
name: string
age: number
}
const App = () => {
const [user, setUser] = useState<User>({
name: 'René',
age: 30,
})
return (
<div>
<h1>User Profile</h1>
<p>Name: {user.name}</p>
<p>Age: {user.age}</p>
<UserEditor
user={user}
setUser={setUser}
/>
</div>
)
}
export default App
Child Component
import { Dispatch, SetStateAction } from 'react'
interface User {
name: string
age: number
}
interface UserEditorProps {
user: User
setUser: Dispatch<SetStateAction<User>>
}
const UserEditor = ({
user,
setUser,
}: UserEditorProps) => {
const handleNameChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setUser((current) => ({
...current,
name: event.target.value,
}))
}
return (
<div>
<label htmlFor="name">
Name
</label>
<input
id="name"
type="text"
value={user.name}
onChange={handleNameChange}
/>
</div>
)
}
export default UserEditor
How it works
- The parent owns the state:
const [user, setUser] = useState({
name: 'René',
age: 30,
})
- The state and setter are passed to the child:
<UserEditor
user={user}
setUser={setUser}
/>
- The child updates the parent's state when the user types:
setUser((current) => ({
...current,
name: event.target.value,
}))
- Because the input is controlled, its value always reflects the current state:
<input
value={user.name}
onChange={handleNameChange}
/>
As the user types, the name property in the parent state updates immediately, and the UI re-renders with the new value.