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>
)
}