跪拜 Guibai
← All articles
React.js · TypeScript · Design

React State Lifting, Explained with a Color Picker

By 烬羽 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

State lifting eliminates an entire class of bugs where two components hold desynchronized copies of the same data. The pattern is a direct application of the single-source-of-truth principle, and getting the details right—immutable updates, string-to-number conversion, callback decoupling—prevents subtle runtime failures that TypeScript alone won't catch.

Summary

When a color picker is split into a display block and slider controls, the RGB state must live in their common parent. That parent becomes the single source of truth, passing the color down as read-only props and receiving updates through an `onColorUpdated` callback. The display component holds no state at all; the control component never mutates props directly but constructs a new object and hands it upward.

Three implementation details trip people up: `event.target.value` is a string, not a number, and the unary `+` operator is the cleanest fix. Spreading the existing color before overriding a single channel prevents data loss, because `setState` replaces the whole object rather than merging fields. Passing `setColor` directly as the callback keeps the control component decoupled from any specific parent, making it reusable and testable.

The checklist is short: if multiple components read the same state, lift it; if a child needs to change it, pass a callback; never let a child mutate state directly; and never store the same truth in two places.

Takeaways
State lifting means moving shared state to the closest common ancestor and passing it down via props.
Changes flow upward through callback props; the child never mutates state directly.
`event.target.value` from a range input is a string, not a number—use the unary `+` to convert it.
Always spread the existing state object before overriding a single field, because `setState` replaces the entire object rather than merging.
Passing `setColor` directly as a callback keeps child components decoupled, reusable, and testable.
A controlled component holds no local state; its rendered output is entirely determined by props.
Storing the same data in two components creates a synchronization bug that state lifting prevents entirely.
Conclusions

The unary `+` operator for string-to-number conversion is concise but divisive: it's terse and idiomatic in some codebases, yet `Number(event.target.value)` is more explicit and less likely to be misread during code review.

The callback pattern's real payoff is testability—a component that receives `onColorUpdated` can be unit-tested with a mock without mounting its entire parent tree, which is harder when the child imports `setColor` directly.

The article's closing question about abstracting three sliders into a generic `ColorSlider` component hints at the next tension: DRY abstraction versus prop-drilling complexity. A generic slider would reduce duplication but introduce an indirection layer that may not be worth it for only three instances.

Concepts & terms
State lifting
A React pattern where shared state is moved to the closest common ancestor of the components that need it, then passed down as props and updated via callback functions.
Controlled component
A component that holds no local state of its own; its rendered output is entirely determined by the props it receives from its parent.
Single source of truth
A software engineering principle stating that every piece of data should have exactly one authoritative location, eliminating synchronization bugs caused by duplicate state.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗