How Side Effects Are Managed in Fre Using useEffect and useLayoutEffect
Fre implements a React-compatible hook system where useLayoutEffect runs synchronously before the DOM is painted to prevent visual flicker, while useEffect runs asynchronously after the commit phase via a microtask scheduler.
Side effects are essential for interfacing with external systems in functional components. In the Fre library—a lightweight, fiber-based React alternative—side effects are managed through a unified hook architecture that separates layout effects (synchronous) from passive effects (asynchronous). This design ensures predictable execution timing while maintaining compatibility with React’s mental model.
Hook Registration and Effect Types
Both useEffect and useLayoutEffect in Fre are thin wrappers around a shared helper function called effectImpl. Located in src/hook.ts at lines 61–71, this function registers effect callbacks into the component’s fiber-based hook list.
export const useEffect = (cb: EffectCallback, deps?: DependencyList) =>
effectImpl(cb, deps!, 'effect'); // ← passive effect
export const useLayout = (cb: EffectCallback, deps?: DependencyList) =>
effectImpl(cb, deps!, 'layout'); // ← layout effect
When invoked, effectImpl stores a tuple containing the callback, its dependency array, and a placeholder for the cleanup function in current.hooks[key]. The hook list is attached directly to the fiber representing the component, enabling persistent state across renders. The second parameter ('effect' or 'layout') acts as a tag that determines when the callback will be executed during the reconciliation lifecycle.
Dependency Tracking with isChanged
Fre optimizes performance by comparing dependency arrays before queueing effects. Before registration, the system checks whether inputs have changed using the isChanged utility located at lines 42–47 in src/hook.ts.
if (isChanged(hook[1], deps)) {
hook[0] = cb;
hook[1] = deps;
current.hooks[key].push(hook as Required<HookEffect>);
}
The isChanged function performs a shallow comparison of the previous and current dependency arrays. If no changes are detected, the effect is skipped entirely. When dependencies differ, the new callback and array replace the old values in the hook tuple (hook[0] and hook[1]), and the effect is pushed into the fiber’s pending effects list for execution.
Execution Order and Scheduling
During the reconciliation phase, Fre processes effects in a specific sequence to guarantee correct timing. After a component’s render function executes, the reconciler “bubbles” up the fiber tree. In src/reconcile.ts at lines 33–38, the bubble function distinguishes between the two effect types:
side(fiber.hooks.layout); // run layout effects now
schedule(() => side(fiber.hooks.effect) as undefined); // schedule passive effects
Layout effects are flushed immediately via the side helper, blocking the main thread until completion. This synchronous execution ensures the DOM has not yet been painted, allowing components to measure layout or adjust styles before the browser displays a frame.
Passive effects are deferred using schedule, which queues them for asynchronous execution. According to src/schedule.ts (lines 17–28), the scheduler uses queueMicrotask, MessageChannel, or setTimeout depending on the environment. This guarantees that passive effects run after the DOM commit phase completes.
Cleanup Handling and Effect Lifecycle
Cleanup functions in Fre follow React’s execution semantics: the previous cleanup runs before the new effect starts. The side function in src/reconcile.ts (lines 34–38) implements this lifecycle:
const side = (effects?: HookEffect[]) => {
effects.forEach(e => e[2] && e[2]()); // call previous cleanup, if any
effects.forEach(e => (e[2] = e[0]())); // run the effect and store new cleanup
effects.length = 0; // empty the list for the next render
};
The function iterates through the effect array twice: first invoking any stored cleanup functions (e[2]), then executing the current effect callback and storing its return value (the new cleanup) back into e[2]. Finally, the effects array is truncated to prevent duplicate executions on subsequent renders.
Practical Implementation Examples
Synchronous Layout Measurement
Use useLayout (Fre’s equivalent to useLayoutEffect) when you need to read DOM dimensions or modify styles before the browser paints to prevent visual inconsistency.
import { h, render, useState, useLayout, useEffect } from 'fre';
function Tooltip() {
const [position, setPosition] = useState({ top: 0, left: 0 });
const ref = useRef<HTMLDivElement>(null);
useLayout(() => {
// Runs synchronously after DOM mutation but before paint
const rect = ref.current!.getBoundingClientRect();
setPosition({ top: rect.bottom + 10, left: rect.left });
}, []);
return <div style={position} ref={ref}>Content</div>;
}
Asynchronous Data Fetching
Use useEffect for side effects that do not impact initial rendering, such as subscriptions or network requests.
function SearchResults({ query }: { query: string }) {
const [results, setResults] = useState<string[]>([]);
useEffect(() => {
// Runs asynchronously after the commit phase
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setResults(data));
// Cleanup runs before the next effect or unmount
return () => controller.abort();
}, [query]); // Only re-run when query changes
return <ul>{results.map(r => <li key={r}>{r}</li>)}</ul>;
}
Summary
useEffectanduseLayoutin Fre are both implemented viaeffectImplinsrc/hook.ts, differing only in their execution timing tags.- Dependency tracking relies on the
isChangedfunction to prevent unnecessary effect re-runs through shallow array comparison. - Layout effects execute synchronously during the
bubblephase insrc/reconcile.ts, ensuring access to the DOM before browser painting. - Passive effects are scheduled asynchronously using the microtask queue defined in
src/schedule.ts, deferring work until after the commit phase. - Cleanup functions are stored in the hook tuple’s third index (
hook[2]) and are always invoked before the corresponding effect runs again or the component unmounts.
Frequently Asked Questions
What is the difference between useEffect and useLayoutEffect in Fre?
useLayoutEffect (exported as useLayout) runs synchronously immediately after the DOM is updated but before the browser paints the screen, making it suitable for measuring layout or preventing visual flicker. useEffect runs asynchronously after the paint cycle, which makes it better suited for data fetching, subscriptions, or other side effects that do not require immediate DOM consistency.
How does Fre determine when to re-run an effect?
Fre uses the isChanged utility in src/hook.ts to perform a shallow comparison of the current and previous dependency arrays provided to the hook. If any dependency has changed reference or value, the effect is queued for execution; otherwise, it is skipped entirely for that render cycle.
When are effect cleanups called in Fre?
Cleanup functions are invoked during the side function execution in src/reconcile.ts, specifically in the first loop that iterates through effects. This occurs before the new effect callback runs, ensuring that resources from the previous render (like timers or event listeners) are released prior to setting up new ones. Cleanups also run when the component is unmounted and the fiber is destroyed.
Does Fre support the same scheduling priorities as React's concurrent mode?
Fre uses a simplified scheduling mechanism based on queueMicrotask, MessageChannel, or setTimeout (as implemented in src/schedule.ts), which provides deferred execution for passive effects but does not implement React’s full concurrent priority lanes or time-slicing features. Layout effects remain strictly synchronous and blocking, identical to React’s behavior.
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 →