Controlled vs. Uncontrolled Components – Advanced Scenarios

Loading

Understanding the nuances between controlled and uncontrolled components is crucial for building robust React applications. While the basic concepts are straightforward, advanced scenarios require deeper consideration.

Core Concepts Recap

Controlled Components:

  • Form data is handled by React state
  • Single source of truth
  • Updates via onChange handlers
  • Immediate validation possible

Uncontrolled Components:

  • Form data is handled by the DOM
  • Accessed via refs
  • Better performance for simple forms
  • Useful for integrating with non-React code

Advanced Scenarios

1. Hybrid Approach (Best of Both Worlds)

Sometimes you need a mix of both approaches:

function HybridInput({ defaultValue, onChange }) {
  const [value, setValue] = useState(defaultValue);
  const inputRef = useRef(null);

  const handleChange = (e) => {
    setValue(e.target.value);
    onChange?.(e.target.value);
  };

  // Expose imperative methods
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    getValue: () => inputRef.current.value,
    setValue: (val) => {
      inputRef.current.value = val;
      setValue(val);
    }
  }));

  return (
    <input
      ref={inputRef}
      value={value}
      onChange={handleChange}
      defaultValue={defaultValue} // Fallback for uncontrolled
    />
  );
}

2. File Inputs (Always Uncontrolled)

function FileUploader({ onFilesSelected }) {
  const fileInput = useRef(null);

  const handleSubmit = (e) => {
    e.preventDefault();
    onFilesSelected(fileInput.current.files);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        type="file" 
        ref={fileInput} 
        multiple 
        // Cannot be controlled in React
      />
      <button type="submit">Upload</button>
    </form>
  );
}

3. Complex Form Optimization

For forms with many fields, uncontrolled components can improve performance:

function LargeForm() {
  const formRef = useRef();

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(formRef.current);
    const data = Object.fromEntries(formData.entries());
    console.log('Submitted:', data);
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      {Array.from({ length: 50 }).map((_, i) => (
        <input
          key={i}
          name={`field-${i}`}
          defaultValue="" // Uncontrolled
        />
      ))}
      <button type="submit">Submit</button>
    </form>
  );
}

4. Third-party Integration

When integrating with non-React libraries (e.g., jQuery plugins):

function DatePicker({ value, onChange }) {
  const inputRef = useRef(null);

  useEffect(() => {
    // Initialize third-party date picker
    const picker = $(inputRef.current).datepicker({
      defaultDate: value,
      onSelect: (date) => {
        onChange(date);
      }
    });

    return () => {
      picker.datepicker('destroy');
    };
  }, []);

  // Controlled sync
  useEffect(() => {
    if (inputRef.current) {
      $(inputRef.current).datepicker('setDate', value);
    }
  }, [value]);

  return <input ref={inputRef} />;
}

5. Dynamic Form Fields

For dynamically added fields, uncontrolled can be simpler:

function DynamicForm() {
  const [fields, setFields] = useState([{ id: 1, defaultValue: '' }]);
  const formRef = useRef();

  const addField = () => {
    setFields(prev => [...prev, { 
      id: Date.now(), 
      defaultValue: '' 
    }]);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const formData = new FormData(formRef.current);
    // Process form data
  };

  return (
    <form ref={formRef} onSubmit={handleSubmit}>
      {fields.map(field => (
        <input
          key={field.id}
          name={`field-${field.id}`}
          defaultValue={field.defaultValue}
        />
      ))}
      <button type="button" onClick={addField}>Add Field</button>
      <button type="submit">Submit</button>
    </form>
  );
}

Performance Considerations

  1. Controlled Components:
  • Re-render on every keystroke
  • Can be optimized with React.memo and proper state management
  • Better for real-time validation
  1. Uncontrolled Components:
  • Fewer re-renders
  • Better for large forms
  • Requires manual syncing when needed

When to Choose Which

Use Controlled When:

  • You need instant validation
  • Form state affects other components
  • You need to programmatically control the input
  • Building complex form logic

Use Uncontrolled When:

  • Integrating with non-React code
  • Dealing with very large forms
  • Performance is critical
  • Working with file inputs
  • Simple forms where you only need values on submit

Advanced Pattern: State Reducer Pattern

For ultimate control over state changes in controlled components:

function inputStateReducer(state, action) {
  switch (action.type) {
    case 'CHANGE':
      if (action.value.includes('test')) {
        return state; // Prevent changes containing 'test'
      }
      return { ...state, value: action.value };
    default:
      return state;
  }
}

function SmartInput({ stateReducer = inputStateReducer }) {
  const [state, dispatch] = useReducer(stateReducer, { value: '' });

  const handleChange = (e) => {
    dispatch({ type: 'CHANGE', value: e.target.value });
  };

  return (
    <input
      value={state.value}
      onChange={handleChange}
    />
  );
}

TypeScript Considerations

interface ControlledInputProps {
  value: string;
  onChange: (value: string) => void;
}

interface UncontrolledInputProps {
  defaultValue?: string;
  ref?: React.Ref<HTMLInputElement>;
}

// Generic form component that can work both ways
type InputProps<T extends boolean> = {
  controlled?: T;
} & (T extends true ? ControlledInputProps : UncontrolledInputProps);

function FlexibleInput<T extends boolean>({
  controlled,
  ...props
}: InputProps<T>) {
  if (controlled) {
    const { value, onChange } = props as ControlledInputProps;
    return <input value={value} onChange={(e) => onChange(e.target.value)} />;
  } else {
    const { defaultValue, ref } = props as UncontrolledInputProps;
    return <input defaultValue={defaultValue} ref={ref} />;
  }
}

Accessibility Considerations

For both patterns:

  • Always associate labels with inputs
  • Provide proper error states and announcements
  • Maintain keyboard navigation
  • Ensure proper focus management
function AccessibleInput({ label, error, ...props }) {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        aria-invalid={!!error}
        aria-describedby={error ? `${id}-error` : undefined}
        {...props}
      />
      {error && (
        <span id={`${id}-error`} className="error">
          {error}
        </span>
      )}
    </div>
  );
}

Choosing between controlled and uncontrolled components depends on your specific use case. Controlled components offer more predictability and control, while uncontrolled components can provide better performance in certain scenarios. The most advanced solutions often combine both approaches strategically.

Leave a Reply

Your email address will not be published. Required fields are marked *