Skip to main content
TypeScript Guide

Using React Context to Avoid Prop-Drilling

React Context is a powerful tool for sharing data across deeply nested components without having to pass props through every level of the tree. When building TypeScript-powered circuit design tools, context lets you centralize configuration and provide well-typed data to any component that needs it.

When to use React Context​

Use context when multiple components need the same piece of state or configuration. Common examples in tscircuit projects include:

  • Design-wide metadata such as board dimensions or authoring info
  • User preferences like measurement units or default footprints

By colocating these values in a context provider, you avoid repetitive prop threading and keep your component interfaces focused on their primary responsibilities.

A Typed Context for Board Settings​

The following example shares board-level configuration with any component that needs it. The BoardSettings interface ensures every consumer receives a strongly typed object.

import { ReactNode, createContext, useContext } from "react"

type BoardSettings = {
boardName: string
boardSize: { width: number; height: number }
defaultFootprints: {
resistor: string
capacitor: string
}
}

const BoardSettingsContext = createContext<BoardSettings | null>(null)

export const BoardSettingsProvider = ({
children,
value,
}: {
children: ReactNode
value: BoardSettings
}) => (
<BoardSettingsContext.Provider value={value}>
{children}
</BoardSettingsContext.Provider>
)

export const useBoardSettings = () => {
const context = useContext(BoardSettingsContext)

if (!context) {
throw new Error("useBoardSettings must be used within a BoardSettingsProvider")
}

return context
}

Consuming the Context​

Components can now read the shared configuration without receiving it through props.

const ResistorList = ({ names }: { names: string[] }) => {
const {
defaultFootprints: { resistor },
} = useBoardSettings()

return (
<group>
{names.map((name) => (
<resistor key={name} name={name} footprint={resistor} resistance="1k" />
))}
</group>
)
}

const DecouplingCapacitor = ({ name }: { name: string }) => {
const {
defaultFootprints: { capacitor },
} = useBoardSettings()

return <capacitor name={name} capacitance="10n" footprint={capacitor} />
}

The components above use the shared default footprints without adding extra props to every layer above them.

Putting It All Together​

Wrap the portion of your circuit tree that needs access to the context with the provider. Any component rendered inside the provider can call useBoardSettings().

export const InstrumentPanel = () => (
<BoardSettingsProvider
value={{
boardName: "Instrumentation Panel",
boardSize: { width: 50, height: 40 },
defaultFootprints: {
resistor: "0402",
capacitor: "0603",
},
}}
>
<board width="50mm" height="40mm" name="InstrumentPanel">
<group name="InputStage">
<ResistorList names={["R1", "R2", "R3"]} />
<DecouplingCapacitor name="C1" />
</group>
</board>
</BoardSettingsProvider>
)

Because every component inside the provider shares the same context, you can introduce additional consumers (for example, status displays or documentation overlays) without modifying intermediate components.

Key Takeaways​

  • Define a context value type that captures the shared configuration.
  • Export both the provider and a custom hook that validates usage.
  • Wrap only the subtree that needs the shared data, keeping providers focused and intentional.

With these patterns, React Context becomes a reliable way to manage shared state in your TypeScript tscircuit projects without the noise of prop drilling.

import { ReactNode, createContext, useContext } from "react"

type BoardSettings = {
boardName: string
boardSize: { width: number; height: number }
defaultFootprints: {
resistor: string
capacitor: string
}
}

const BoardSettingsContext = createContext<BoardSettings | null>(null)

const BoardSettingsProvider = ({
children,
value,
}: {
children: ReactNode
value: BoardSettings
}) => (
<BoardSettingsContext.Provider value={value}>
{children}
</BoardSettingsContext.Provider>
)

const useBoardSettings = () => {
const context = useContext(BoardSettingsContext)

if (!context) {
throw new Error("useBoardSettings must be used within a BoardSettingsProvider")
}

return context
}

const ResistorList = ({ names }: { names: string[] }) => {
const {
defaultFootprints: { resistor },
} = useBoardSettings()

return (
<group name="Resistors">
{names.map((name) => (
<resistor key={name} name={name} footprint={resistor} resistance="1k" />
))}
</group>
)
}

const DecouplingCapacitor = ({ name }: { name: string }) => {
const {
defaultFootprints: { capacitor },
} = useBoardSettings()

return <capacitor name={name} capacitance="10n" footprint={capacitor} />
}

export default function InstrumentPanel() {
return (
<BoardSettingsProvider
value={{
boardName: "Instrumentation Panel",
boardSize: { width: 50, height: 40 },
defaultFootprints: {
resistor: "0402",
capacitor: "0603",
},
}}
>
<board width="50mm" height="40mm" name="InstrumentPanel">
<ResistorList names={["R1", "R2", "R3"]} />
<DecouplingCapacitor name="C1" />
</board>
</BoardSettingsProvider>
)
}
Schematic Circuit Preview