# How OpenCut Handles State Management Across React Components

> Discover how OpenCut manages state across React components using Context API and custom hooks. Learn its efficient, library-free approach to shared state.

- Repository: [OpenCut.app/OpenCut](https://github.com/OpenCut-app/OpenCut)
- Tags: deep-dive
- Published: 2026-07-01

---

**OpenCut uses React's built-in hooks combined with the Context API to manage state, avoiding external state libraries like Redux or Zustand by encapsulating shared state in provider components and exposing it via custom hooks.**

OpenCut is an open-source video editing application built with React that takes a lightweight approach to state management. Instead of integrating complex external stores, the codebase relies entirely on React's native state primitives and the Context API to share data between components. This architecture keeps the component tree modular and avoids prop drilling while maintaining type safety across the TypeScript codebase.

## Local Component State with Hooks

For UI primitives that don't require shared access to their data, OpenCut uses standard **React hooks** like `useState`, `useCallback`, and `useMemo`. This pattern keeps component logic self-contained and performant.

The carousel component in [`apps/web/src/components/ui/carousel.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/carousel.tsx) demonstrates this approach by maintaining scroll state locally:

```tsx
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)

```

This local state management works perfectly when state doesn't need to escape the component boundary, eliminating unnecessary re-renders in unrelated parts of the application.

## Context API for Shared State

When state must be accessible to many nested components, OpenCut implements the **Context API** pattern. A dedicated context is created at the top level of a UI primitive, with a custom hook abstracting the `useContext` call for consumers.

### Sidebar State Management

The sidebar implementation in [`apps/web/src/components/ui/sidebar.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/sidebar.tsx) showcases the complete provider pattern. It creates a `SidebarContext` that tracks expanded/collapsed states and mobile-specific behavior:

```tsx
const SidebarContext = React.createContext<SidebarContextProps | null>(null)

function SidebarProvider({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, … }) {
  const isMobile = useIsMobile()
  const [openMobile, setOpenMobile] = React.useState(false)
  const [_open, _setOpen] = React.useState(defaultOpen)
  
  const open = openProp ?? _open
  
  const setOpen = React.useCallback((value) => {
    // implementation details
  }, [setOpenProp, open])

  const toggleSidebar = React.useCallback(() => {
    return isMobile ? setOpenMobile((o) => !o) : setOpen((o) => !o)
  }, [isMobile, setOpen, setOpenMobile])

  const contextValue = React.useMemo(() => ({
    state: open ? "expanded" : "collapsed",
    open,
    setOpen,
    isMobile,
    openMobile,
    setOpenMobile,
    toggleSidebar,
  }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar])

  return (
    <SidebarContext.Provider value={contextValue}>
      {children}
    </SidebarContext.Provider>
  )
}

function useSidebar() {
  const context = React.useContext(SidebarContext)
  if (!context) throw new Error("useSidebar must be used within a SidebarProvider.")
  return context
}

```

The `useSidebar` hook enforces that consumers must be wrapped in a `SidebarProvider`, providing clear error boundaries during development.

### Controlled vs Uncontrolled Patterns

OpenCut components support both **controlled** and **uncontrolled** modes through a consistent prop interface. Components accept `open`, `onOpenChange`, and `defaultOpen` props, with the provider internally deciding whether to use external values or internal state:

```tsx
const open = openProp ?? _open

```

This pattern allows developers to either let the component manage its own state or take full control from a parent component, making the primitives flexible for different use cases.

## Specialized Context Patterns

### Shared Styling with ToggleGroup

The toggle group component in [`apps/web/src/components/ui/toggle-group.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/toggle-group.tsx) uses context to share styling props (`variant`, `size`, `spacing`, `orientation`) from the group container to individual items:

```tsx
const ToggleGroupContext = React.createContext<
  VariantProps<typeof toggleVariants> & {
    spacing?: number
    orientation?: "horizontal" | "vertical"
  }
>({ size: "default", variant: "default", spacing: 0, orientation: "horizontal" })

function ToggleGroup({ variant, size, spacing = 0, orientation = "horizontal", …props }) {
  return (
    <ToggleGroupPrimitive …>
      <ToggleGroupContext.Provider value={{ variant, size, spacing, orientation }}>
        {children}
      </ToggleGroupContext.Provider>
    </ToggleGroupPrimitive>
  )
}

```

Child items consume this context to inherit styling without explicit prop drilling:

```tsx
function ToggleGroupItem({ variant = "default", size = "default", …props }) {
  const context = React.useContext(ToggleGroupContext)
  // Merge context values with item-specific props
}

```

### Dual Context Form Architecture

Form handling in [`apps/web/src/components/ui/form.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/form.tsx) demonstrates a sophisticated **dual context pattern**. `FormFieldContext` carries the field name while `FormItemContext` manages generated IDs:

```tsx
const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)
const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue)

```

Sub-components consume both contexts to coordinate field registration and accessibility attributes without exposing implementation details to parent components.

### Static Configuration with ChartContext

The chart component uses a lightweight context in [`apps/web/src/components/ui/chart.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/chart.tsx) to share static configuration (colors, icons, labels) between the container and tooltip components:

```tsx
const ChartContext = React.createContext<ChartContextProps | null>(null)

function ChartContainer({ config, … }) {
  return (
    <ChartContext.Provider value={{ config }}>
      {children}
    </ChartContext.Provider>
  )
}

```

## Practical Usage Examples

Implementing the sidebar in your application requires wrapping the layout in the provider:

```tsx
import { SidebarProvider, Sidebar, SidebarTrigger, SidebarContent } from "#/components/ui/sidebar"

function AppLayout() {
  return (
    <SidebarProvider>
      <Sidebar>
        <SidebarContent>
          {/* navigation items */}
        </SidebarContent>
      </Sidebar>
      <SidebarTrigger />
    </SidebarProvider>
  )
}

```

The `SidebarTrigger` component internally calls `useSidebar()` to access the toggle function, demonstrating how the context abstraction simplifies component interaction.

For toggle groups that require consistent styling across items:

```tsx
import { ToggleGroup, ToggleGroupItem } from "#/components/ui/toggle-group"

function FontSizeSelector() {
  return (
    <ToggleGroup variant="outline" size="sm" spacing={2} orientation="horizontal">
      <ToggleGroupItem value="small">S</ToggleGroupItem>
      <ToggleGroupItem value="medium">M</ToggleGroupItem>
      <ToggleGroupItem value="large">L</ToggleGroupItem>
    </ToggleGroup>
  )
}

```

## Summary

- **OpenCut avoids external state libraries**, relying solely on React's `useState`, `useCallback`, `useMemo`, and the Context API for all state management needs.
- **Context providers** wrap complex UI primitives (sidebar, forms, charts) and expose state through custom hooks like `useSidebar()` and `useChart()`.
- **Controlled/uncontrolled patterns** allow components to accept `open`, `defaultOpen`, and `onOpenChange` props, internally managing state only when external control isn't provided.
- **Memoization** prevents unnecessary re-renders by wrapping provider values in `React.useMemo` before passing them to context providers.
- **Local state** handles isolated component concerns (like carousel scroll position) without context overhead.

## Frequently Asked Questions

### Does OpenCut use Redux or Zustand for state management?

No, OpenCut does not integrate external state management libraries like Redux, Zustand, or Jotai. According to the OpenCut-app/OpenCut repository source code, all state lives inside the React component tree and is propagated via the Context API when shared between components, or kept local with `useState` when isolated.

### How does OpenCut handle form state across multiple fields?

OpenCut implements a **dual context pattern** in [`apps/web/src/components/ui/form.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/form.tsx). `FormFieldContext` tracks the field name and registration while `FormItemContext` manages unique IDs for accessibility. These contexts allow `FormLabel`, `FormControl`, and `FormMessage` components to coordinate without requiring explicit props at every level, eliminating prop drilling in complex forms.

### What is the controlled vs uncontrolled pattern in OpenCut components?

OpenCut UI primitives accept `value` (or `open`), `defaultValue` (or `defaultOpen`), and `onChange` (or `onOpenChange`) props. When the controlled value prop is provided, the component uses that external state; otherwise, it falls back to internal `useState` initialized with the default value. This pattern is clearly implemented in [`apps/web/src/components/ui/sidebar.tsx`](https://github.com/OpenCut-app/OpenCut/blob/main/apps/web/src/components/ui/sidebar.tsx) where `const open = openProp ?? _open` determines which state source to use.

### Why does OpenCut wrap context values in useMemo?

Provider components wrap their context values in `React.useMemo` to maintain referential equality across renders. Without memoization, every render would create a new context value object, triggering re-renders in all consuming components regardless of whether the actual state data changed. This optimization is visible in the `SidebarProvider` implementation where the entire context object is memoized based on its constituent state values.