How Fre Implements useContext for Context Propagation: A Deep Dive into the Source Code
Fre implements useContext by combining a subscriber-based notification system with fiber tree traversal, where createContext creates a provider component that maintains a Set of subscriber callbacks, and useContext registers the current component as a subscriber while using getBoundary to locate the nearest provider in the fiber tree.
Fre is a lightweight React alternative that implements a fiber-based reconciliation engine. Its context system demonstrates how modern JavaScript frameworks achieve efficient state propagation without prop drilling. This article examines the exact implementation details found in src/hook.ts and src/reconcile.ts, revealing how Fre manages context subscriptions and tree traversal.
The Architecture Behind Fre's Context System
Fre's context implementation relies on two primary hooks: createContext for defining providers and useContext for consuming values. The system leverages Fre's fiber architecture to track component relationships and manage subscriptions.
createContext: Building the Provider Component
The createContext function in src/hook.ts (lines 14-25) generates a context provider component that manages value storage and subscriber notifications:
// src/hook.ts L14-L25
export function createContext<T>(initialValue: T) {
const context = {
initialValue,
Provider: (props: { value: T; children: any }) => {
const ref = useRef(props.value)
const subs = useMemo(() => new Set<() => void>(), [])
if (ref.current !== props.value) {
ref.current = props.value
subs.forEach(sub => sub())
}
return props.children
}
}
return context
}
The provider uses useRef to store the current value and useMemo to maintain a Set of subscriber callbacks. When the value prop changes, the provider updates the ref and iterates through all subscribers, triggering re-renders in consuming components.
useContext: Consuming and Subscribing
The useContext implementation in src/hook.ts (lines 29-40) handles subscriber registration and value retrieval:
// src/hook.ts L29-L40
export function useContext<T>(context: any): T {
const update = useReducer(null, null)[1]
const fiber = useFiber()
useEffect(() => {
const provider = getBoundary(fiber, context.Provider)
if (provider) {
const subs = provider.hooks.list[1][0] as Set<() => void>
subs.add(update)
return () => subs.delete(update)
}
}, [context])
const provider = getBoundary(fiber, context.Provider)
return provider
? (provider.hooks.list[0][0] as { current: T }).current
: context.initialValue
}
This hook uses useReducer(null, null) to generate a stable update function that serves as the subscriber callback. It then uses useEffect to register this callback with the nearest provider and returns a cleanup function that removes the subscription on unmount.
Fiber Tree Traversal and Boundary Detection
Fre's context system relies on the fiber tree to locate providers. Two key utilities in src/reconcile.ts enable this traversal.
useFiber: Accessing the Current Fiber
The useFiber function (lines 27-28) provides hooks with access to the currently executing fiber:
// src/reconcile.ts L27-L28
export const useFiber = () => currentFiber
This simple utility returns the global currentFiber variable, allowing useContext to know which component is currently rendering and where to start climbing the tree.
getBoundary: Locating the Nearest Provider
The getBoundary function (lines 46-52) walks up the fiber parent chain to find the closest ancestor of a specific type:
// src/reconcile.ts L46-L52
const getBoundary = (fiber: Fiber, type: any) => {
while (fiber) {
if (fiber.type === type) return fiber
fiber = fiber.parent
}
return null
}
This linear traversal climbs from the current fiber to the root, checking each node's type against the context provider component. Once found, useContext accesses the provider's hook slots to retrieve the current value and subscriber set.
The Subscription and Notification Flow
Understanding the complete data flow reveals how Fre achieves efficient context propagation without unnecessary re-renders:
-
Provider initialization – When a context provider renders,
createContextstores the value infiber.hooks.list[0][0](a ref) and initializes an emptySetinfiber.hooks.list[1][0]for subscribers. -
Consumer registration – When a component calls
useContext, it generates an updater function viauseReducer, then usesuseEffectto add this updater to the provider's subscriber set. The effect returns a cleanup function that removes the subscriber on unmount. -
Value updates – When the provider receives a new
valueprop, it compares it against the ref. If changed, it updates the ref and iterates throughsubs, calling each subscriber function. -
Re-render triggering – The subscriber functions are the updaters returned by
useReducer(null, null). When invoked, they trigger a re-render of the consuming component, which then reads the new value from the provider's ref viagetBoundary.
This design ensures that only components explicitly calling useContext re-render when the context changes, and the subscription mechanism leverages Fre's existing hook infrastructure without additional overhead.
Practical Implementation Examples
Defining a Context
Create a context using createContext with a default value:
import { createContext } from "fre";
export const ThemeContext = createContext<string>("light");
Providing Context Values
Wrap components with the Provider component returned by createContext:
import { ThemeContext } from "./theme-context";
import { useState } from "fre";
function App() {
const [theme, setTheme] = useState("light");
return (
<ThemeContext value={theme}>
<Toolbar setTheme={setTheme} />
</ThemeContext>
);
}
Consuming Context
Use useContext to access the current value and subscribe to updates:
import { useContext } from "fre";
import { ThemeContext } from "./theme-context";
function Toolbar({ setTheme }) {
const theme = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Switch to {theme === "light" ? "dark" : "light"} mode
</button>
);
}
When the button triggers a theme change, the provider detects the new value, notifies all subscribers, and Toolbar re-renders with the updated theme.
Summary
- Fre implements useContext using a subscriber pattern where providers maintain a Set of callback functions and consumers register updaters via useReducer.
- createContext generates a provider component that stores values in useRef and subscribers in useMemo, iterating through the subscriber set when values change.
- useContext leverages getBoundary to walk the fiber parent chain (src/reconcile.ts L46-L52) and locate the nearest provider, then reads the current value from the provider's hook slots.
- Fiber traversal occurs through useFiber (src/reconcile.ts L27-28) and getBoundary, enabling efficient O(depth) lookup of context providers without virtual DOM overhead.
- Subscription lifecycle is managed through useEffect, ensuring subscribers are added on mount and removed on unmount, preventing memory leaks and unnecessary updates.
Frequently Asked Questions
How does Fre's useContext differ from React's implementation?
Fre's useContext achieves the same semantic behavior as React—providing a way to pass data through the component tree without prop drilling—but uses a lighter-weight implementation. While React uses a separate context dependency system and potentially complex propagation heuristics, Fre implements context propagation through a simple subscriber Set stored in the provider's hook slot (fiber.hooks.list[1][0]). When the provider value changes, Fre iterates through this Set and calls each subscriber directly, triggering a re-render via the updater function created by useReducer(null, null).
Where does Fre store the context value and subscriber list?
Fre stores context data directly in the provider fiber's hook slots. According to the source code in src/hook.ts, the current value lives in fiber.hooks.list[0][0] as a ref object ({ current: T }), while the subscriber Set resides in fiber.hooks.list[1][0]. This design leverages Fre's existing hook infrastructure—specifically the list array that stores hook state—eliminating the need for separate context storage mechanisms. When useContext calls getBoundary to find the provider, it accesses these slots directly to retrieve the current value and register new subscribers.
How does Fre handle context updates and re-renders?
When a context provider receives a new value prop, the component created by createContext compares the new value against the previous one stored in a useRef. If the values differ, the ref updates and the component iterates through the subs Set (stored in fiber.hooks.list[1][0]), calling each subscriber function. These subscriber functions are the updaters returned by useReducer(null, null) in consuming components. Invoking an updater triggers Fre's reconciliation process for that specific fiber, causing the component to re-render and call useContext again, which retrieves the updated value from the provider's ref in fiber.hooks.list[0][0].
What files contain the core context implementation in Fre?
The context system spans three primary files in Fre's source code. src/hook.ts contains the main implementation including createContext (lines 14-25) and useContext (lines 29-40), along with hook slot management utilities. src/reconcile.ts provides the fiber traversal mechanisms through useFiber (lines 27-28) and getBoundary (lines 46-52), which enable useContext to locate provider components in the tree. src/type.ts defines the TypeScript interfaces including ContextType, Fiber, and HookList that shape the data structures used throughout the context system. Together, these files implement a complete context propagation mechanism in under 100 lines of code.
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 →