# How Side Effects Are Managed in Fre Using useEffect and useLayoutEffect

> Learn how Fre manages side effects with useEffect and useLayoutEffect. Understand their timing for DOM painting and asynchronous updates to optimize your Fre apps.

- Repository: [frejs/fre](https://github.com/frejs/fre)
- Tags: deep-dive
- Published: 2026-03-02

---

**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`](https://github.com/frejs/fre/blob/main/src/hook.ts) at lines 61–71, this function registers effect callbacks into the component’s fiber-based hook list.

```typescript
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`](https://github.com/frejs/fre/blob/main/src/hook.ts).

```typescript
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`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) at lines 33–38, the `bubble` function distinguishes between the two effect types:

```typescript
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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) (lines 34–38) implements this lifecycle:

```typescript
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.

```tsx
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.

```tsx
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

- **`useEffect`** and **`useLayout`** in Fre are both implemented via `effectImpl` in [`src/hook.ts`](https://github.com/frejs/fre/blob/main/src/hook.ts), differing only in their execution timing tags.
- **Dependency tracking** relies on the `isChanged` function to prevent unnecessary effect re-runs through shallow array comparison.
- **Layout effects** execute synchronously during the `bubble` phase in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts), ensuring access to the DOM before browser painting.
- **Passive effects** are scheduled asynchronously using the microtask queue defined in [`src/schedule.ts`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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`](https://github.com/frejs/fre/blob/main/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.