How OpenCut Handles State Management Across React Components
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 demonstrates this approach by maintaining scroll state locally:
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 showcases the complete provider pattern. It creates a SidebarContext that tracks expanded/collapsed states and mobile-specific behavior:
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:
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 uses context to share styling props (variant, size, spacing, orientation) from the group container to individual items:
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:
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 demonstrates a sophisticated dual context pattern. FormFieldContext carries the field name while FormItemContext manages generated IDs:
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 to share static configuration (colors, icons, labels) between the container and tooltip components:
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:
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:
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()anduseChart(). - Controlled/uncontrolled patterns allow components to accept
open,defaultOpen, andonOpenChangeprops, internally managing state only when external control isn't provided. - Memoization prevents unnecessary re-renders by wrapping provider values in
React.useMemobefore 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. 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →