React State Management Patterns: The "Uncontrolled with Callback" Approach
Photo by Christophe Hautier on Unsplash

React State Management Patterns: The "Uncontrolled with Callback" Approach

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

  1. Clear Separation of Concerns: Each component manages its own internal state while enabling coordination.
  2. Flexible Integration: Components can be used as simple uncontrolled components or integrated with external state when needed.
  3. Improved Developer Experience: The default behavior is simple, with additional complexity only when required.
  4. Better Performance: Minimizes unnecessary re-renders from parent components.

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:

  • The child component uses the state for UI concerns (calendar display, user interaction)
  • The parent uses it for different purposes (data fetching, form submission)

This separation allows each component to focus on its primary responsibilities.

When to Apply This Pattern

This pattern works best when:

  • Components have their own UI-specific state needs
  • You want to maintain flexibility for various use cases
  • The state primarily belongs to the component but occasionally needs external coordination

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




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

3 周

BTW, this post was based on a conversation with Andrew R. H. Quinn and with #claude #ai ??.

回复

要查看或添加评论,请登录

Manav Misra的更多文章

社区洞察

其他会员也浏览了