跪拜 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 ↗