React State Management Patterns: The "Uncontrolled with Callback" Approach
Manav Misra
Senior Software Developer | Enterprise Application Architect | Engineering Excellence Leader Transforming Legacy Systems into Modern Architecture | Tech Educator | Clean Code Advocate ? | Sports Entertainment Fan
Balancing simplicity and flexibility in React component design
One of the most important decisions in React component design is determining who owns and manages state. We default to uni-directional data flow in React. However, as we build more complex applications and especially custom component libraries things get a bit more nuanced. An elegant pattern that balances simplicity with flexibility: the "uncontrolled with callback" pattern.
First, Some Fundamentals
Uni-directional Data Flow is a core React principle where data flows down from parent to child components through props, while events flow up through callbacks. This creates a predictable flow that's easier to debug and reason about.
Component Coupling refers to how dependent components are on each other. Tightly coupled components are harder to reuse but sometimes easier to build initially. Decoupled components are more flexible but require thoughtful design.
Hence, the challenge to balance ??.
The Challenges of Strict Uni-directional Flow
Let's examine a standard approach with a DatePicker component:
// Parent component with full control
function BookingForm() {
// Parent manages all state
const [date, setDate] = useState(new Date());
return (
<DatePicker
value={date}
onChange={setDate}
/>
);
}
// Fully controlled DatePicker
function DatePicker({ value, onChange }) {
// No internal state - fully dependent on props
return (
<div>
{/* Calendar UI that uses value and calls onChange */}
</div>
);
}
While this follows uni-directional data flow perfectly, it forces the parent to always manage the date state, even when it doesn't need to. This creates unnecessary coupling and complexity.
The Problems with Tight Coupling
The fully controlled approach becomes problematic in larger applications:
// Parent with multiple state dependencies
function BookingForm() {
// Parent must manage ALL state for child components
const [date, setDate] = useState(new Date());
const [time, setTime] = useState("12:00");
const [guests, setGuests] = useState(2);
// ... potentially many more states
return (
<>
<DatePicker value={date} onChange={setDate} />
<TimePicker value={time} onChange={setTime} />
<GuestCounter value={guests} onChange={setGuests} />
</>
);
}
As components multiply, the parent becomes overburdened with state management that could be handled internally by each component. This tight coupling makes components less reusable and the system more brittle.
The "Uncontrolled with Callback" Pattern
This pattern strikes a balance between fully controlled components (where parents manage all state) and completely uncontrolled ones (where internal state is inaccessible).
How It Works
1. Component manages its own internal state
2. Component accepts an initial value via props
3. Component notifies parent of changes via callback
4. Parent can respond to changes but doesn't need to track state unless needed
领英推荐
// DatePicker with internal state but external notification
function DatePicker({ initialDate = new Date(), handleChange }) {
// Internal state management
const [date, setDate] = useState(initialDate);
// Update internal state AND notify parent
const updateDate = (newDate) => {
setDate(newDate);
if (handleChange) {
handleChange(newDate);
}
};
return (
<div>
{/* Calendar UI that uses internal state and calls updateDate */}
</div>
);
}
Example
// DatePicker component (simplified)
function DatePicker({ initialDate = new Date(), handleChange }) {
// Internal state management
const [date, setDate] = useState(initialDate);
// Notify parent when date changes
const updateDate = (newDate) => {
setDate(newDate);
if (handleChange) {
handleChange(newDate);
}
};
return (
/* DatePicker UI implementation */
);
}
// Parent component
function BookingForm() {
// Parent only tracks state if it needs it
const [selectedDate, setSelectedDate] = useState(new Date());
// Integration with data fetching
const { data } = useSWR(`/api/availability?date=${format(selectedDate, 'yyyy-MM-dd')}`);
return (
<DatePicker
initialDate={selectedDate}
handleChange={(date) => setSelectedDate(date)}
/>
);
}
The parent can now be significantly simplified:
// Simplified parent that only tracks what it needs
function BookingForm() {
// Optional: track date only if needed for other purposes
const [selectedDate, setSelectedDate] = useState(new Date());
// Data fetching based on selected date
const { data } = useSWR(`/api/availability?date=${format(selectedDate, 'yyyy-MM-dd')}`);
return (
<DatePicker
initialDate={selectedDate}
handleChange={(date) => setSelectedDate(date)}
/>
);
}
Benefits of This Pattern
Addressing the "Double Tracking" Concern
The main potential drawback is the duplication of state in both parent and child components. However, this controlled redundancy serves a purpose:
This separation allows each component to focus on its primary responsibilities.
When to Apply This Pattern
This pattern works best when:
Many of React's built-in components follow this pattern (consider uncontrolled form inputs with onChange handlers), proving its effectiveness and idiomatic nature in the React ecosystem.
What's your favorite React pattern for balancing control and simplicity?
#ReactJS #FrontendDevelopment #WebDevelopment #JavaScript #ComponentDesign #StateManagement
Senior Software Developer | Enterprise Application Architect | Engineering Excellence Leader Transforming Legacy Systems into Modern Architecture | Tech Educator | Clean Code Advocate ? | Sports Entertainment Fan
3 周BTW, this post was based on a conversation with Andrew R. H. Quinn and with #claude #ai ??.