Skip to content

Instantly share code, notes, and snippets.

@paoloricciuti
Created March 27, 2025 20:10
Show Gist options
  • Select an option

  • Save paoloricciuti/31e4e1ee0cbe44970e51b31395f0d612 to your computer and use it in GitHub Desktop.

Select an option

Save paoloricciuti/31e4e1ee0cbe44970e51b31395f0d612 to your computer and use it in GitHub Desktop.
Gemini explains svelte reactivity

Signal-Based Reactivity System

This document provides an overview of the signal-based reactivity system implemented in this project. This system allows for efficient and fine-grained updates in response to data changes. It is inspired by reactive programming paradigms and utilizes key concepts like signals, derived values, and effects.

Core Concepts

The reactivity system revolves around three main concepts:

  1. Signals (Sources): These are the fundamental building blocks of the system. A signal holds a value that can change over time. When a signal's value is updated, it notifies all the parts of the system that depend on it.

  2. Derived Values (Deriveds): These are signals whose values are automatically computed based on the values of other signals. When the dependencies of a derived value change, its value is automatically recomputed.

  3. Effects: These are functions that run in response to changes in signals. They are used to perform side effects, such as updating the DOM, logging data, or making API calls, whenever the signals they depend on change.

File Breakdown

This reactivity system is defined by the following files:

deriveds.js

This file contains the logic for creating and managing derived signals. Derived signals are read-only signals whose values are derived from other signals.

Key Functions:

  • derived(fn): Creates a new derived signal. The provided function fn is executed to compute the derived value. This function automatically tracks any other signals accessed within it. When any of these dependency signals change, the fn will be re-executed, and the derived signal's value will be updated.
    • It handles scenarios where derived signals are created within other reactions (deriveds or effects) to establish ownership and dependency trees.
    • It uses flags to track the state of the derived signal (e.g., DIRTY, UNOWNED).
  • user_derived(fn): Similar to derived, but it also pushes the newly created derived signal onto a runtime stack. This is likely used for specific lifecycle management or initial value tracking, especially in the context of components.
  • derived_safe_equal(fn): Creates a derived signal that uses a "safe equality" check instead of strict equality to determine if the derived value has changed. This is useful when comparing complex objects where reference equality might not be sufficient.
  • destroy_derived_effects(derived): When a derived signal is no longer needed, this function cleans up any effects that were created inside the derived's computation function. This helps prevent memory leaks and ensures proper resource management.
  • update_derived(derived): This internal function is responsible for re-evaluating the derived signal's computation function. It handles dependency tracking, checks if the new value is different from the old one, updates the derived signal's value and write version, and marks any dependent reactions as needing updates.
    • It also includes logic to detect and prevent infinite recursion in derived computations.

effects.js

This file defines how effects are created and managed. Effects are used to perform side effects in response to signal changes.

Key Functions:

  • user_effect(fn): Creates a new effect. The provided function fn is executed initially and whenever any of the signals it depends on change.
    • It includes validation to ensure effects are created within a valid reactive context (e.g., not orphaned or during teardown).
    • It handles deferring the execution of effects within certain contexts (like component mounting).
  • user_pre_effect(fn): Creates an effect that is intended to run synchronously before other effects, often used for setup or tasks that need to happen immediately.
  • inspect_effect(fn): Creates a special effect used for debugging and introspection of the reactivity system. These effects run immediately when their dependencies change.
  • effect_root(fn): Creates a root effect. Any effects created within the provided function fn are considered children of this root effect. When the root effect is destroyed, all its child effects are also destroyed, providing a mechanism for managing the lifecycle of related effects.
  • component_root(fn): Similar to effect_root, but specifically designed for managing effects within a component. It includes functionality for handling outro transitions before destroying child effects.
  • effect(fn): A lower-level function for creating effects with specific flags and configurations.
  • legacy_pre_effect(deps, fn) and legacy_pre_effect_reset(): These functions seem to be related to a previous or legacy reactivity system and are likely kept for compatibility or specific use cases.
  • render_effect(fn): Creates an effect that is specifically intended for rendering updates. These effects typically run synchronously.
  • template_effect(fn, thunks, d = derived): Creates an effect that depends on a set of derived values (represented by thunks). This is likely used in the context of rendering dynamic content in templates.
  • block(fn, flags = 0): Creates a block effect, which might have specific characteristics or optimizations related to rendering blocks of content.
  • branch(fn, push = true): Creates a branch effect, likely used for conditional rendering logic.
  • execute_effect_teardown(effect): If an effect has a teardown function (a function returned by the main effect function), this function executes it when the effect is being destroyed. This is used for cleanup tasks like removing event listeners.
  • destroy_effect_children(signal, remove_dom = false): Recursively destroys all child effects of a given effect. It can optionally remove associated DOM nodes.
  • destroy_block_effect_children(signal): Destroys child effects of a block effect, potentially excluding certain types of effects (like branch effects).
  • destroy_effect(effect, remove_dom = true): Destroys a specific effect. This involves executing its teardown function, destroying its child effects, removing associated DOM nodes (if applicable), and unlinking it from the reactive graph.
  • unlink_effect(effect): Detaches an effect from its parent in the effect tree.
  • pause_effect(effect, callback) and resume_effect(effect): These functions are used to temporarily pause and resume the execution of an effect and its children, often used in conjunction with animations or transitions.

sources.js

This file defines the fundamental source signals, which hold mutable values and trigger updates when their values change.

Key Functions:

  • source(v, stack): Creates a new source signal with an initial value v. This is the most basic type of signal that directly holds a mutable value.
    • In development mode with tracing enabled, it can capture the creation stack for debugging purposes.
  • state(v, stack): Similar to source, but it also pushes the newly created signal onto a runtime stack. This is likely the primary way to create mutable state within components or the application.
  • mutable_source(initial_value, immutable = false): Creates a source signal that is intended to be mutated. The immutable flag can be used to control whether the signal uses strict equality checks for updates.
    • It also handles binding the signal to the component context in legacy mode for lifecycle callbacks.
  • mutate(source, value): Provides a way to update a source signal's value by applying a mutation to its current value, while ensuring that the update triggers reactivity.
  • set(source, value, should_proxy = false): Updates the value of a source signal to the provided value.
    • It includes checks to prevent unsafe mutations of state within derived computations or block effects in runes mode.
    • It uses an equality check to avoid triggering updates if the new value is the same as the old one (using the signal's equals method).
    • It increments a write version counter to track changes.
    • It marks all dependent reactions (deriveds and effects) as dirty.
    • It handles immediate updates for inspect effects.
  • internal_set(source, value): A lower-level function that performs the actual setting of the source signal's value and handles the update logic.
  • update(source, d = 1) and update_pre(source, d = 1): Convenience functions for incrementing or decrementing the numerical value of a source signal.
  • mark_reactions(signal, status): This internal function iterates through all the reactions (deriveds and effects) that depend on the given signal and marks them with the specified status (e.g., DIRTY or MAYBE_DIRTY), indicating that they need to be re-evaluated or re-run.

props.js

This file deals with how component properties (props) are handled within the reactivity system, allowing data to flow into components and be reactive.

Key Functions:

  • prop(props, key, flags, fallback): This is the core function for defining a component property.
    • It takes the component's props object, the key of the property, flags indicating its behavior (e.g., PROPS_IS_IMMUTABLE, PROPS_IS_BINDABLE, PROPS_IS_RUNES, PROPS_IS_UPDATED), and an optional fallback value.
    • It handles cases where the prop might be bound (using bind: syntax), in which case it captures the store binding.
    • It provides a getter function to access the current value of the prop.
    • If the prop is marked as PROPS_IS_UPDATED, it returns a setter function that allows the component to update the prop's value, which will then propagate back to the parent component if it's bound.
    • It handles synchronization between the parent and child component's prop values, including logic for local temporary overrides.
    • It also manages fallback values when the prop is not provided by the parent.
  • rest_props(props, exclude, name) and legacy_rest_props(props, exclude): These functions handle the collection of "rest" props (props passed to a component that are not explicitly declared). They return a proxy that allows accessing these props. The legacy version has additional logic for handling updates and deletions.
  • spread_props(...props): This function takes an array of prop objects (or functions that return prop objects) and creates a proxy that merges them into a single props object. This is used for the spread syntax in templates.
  • update_prop(fn, d = 1) and update_pre_prop(fn, d = 1): These utility functions are similar to the update functions in sources.js but are specifically designed for updating prop values.

Usage Examples

Here are some basic examples to illustrate how to use this reactivity system:

Creating a Source Signal:

import { state } from './sources.js';

const count = state(0); // Creates a signal with an initial value of 0

console.log(count.v); // Access the current value (0)

Updating a Source Signal:
import { set } from './sources.js';
import { count } from './examples'; // Assuming the above example

set(count, 1); // Updates the value of the count signal to 1
console.log(count.v); // Output: 1

Creating a Derived Signal:
import { derived } from './deriveds.js';
import { count } from './examples'; // Assuming the above example

const doubleCount = derived(() => count.v * 2);

console.log(doubleCount.v); // Output: 2 (based on the current count value of 1)

Creating an Effect:
import { user_effect } from './effects.js';
import { count } from './examples'; // Assuming the above example

user_effect(() => {
  console.log(`Count is: ${count.v}`); // This will log "Count is: 1" when count changes
});

// When count is updated:
import { set } from './sources.js';
set(count, 2); // This will trigger the effect, logging "Count is: 2"

Component Property:
// In a component's code:
import { prop } from './props.js';

export function MyComponent(props) {
  const name = prop(props, 'name');

  // Access the prop value:
  console.log(name());
}

// When using the component:
<MyComponent name="Alice" />;

Key Features and Principles
 * Fine-Grained Dependency Tracking: The system automatically tracks which signals are used within derived computations and effect functions. This ensures that updates are only triggered when the relevant dependencies change.
 * Lazy Evaluation: Derived signals are typically evaluated lazily, meaning their values are only computed when they are actually needed by other parts of the system (like effects or when their value is explicitly accessed).
 * Efficient Updates: When a source signal changes, only the derived signals and effects that directly depend on it are re-evaluated or re-run, leading to efficient and minimal updates.
 * Reactive Context Management: The system uses a concept of an "active reaction" and "active effect" to manage the context in which derived signals and effects are created, ensuring proper dependency tracking and ownership.
 * Lifecycle Management: Features like effect roots and component roots provide mechanisms for managing the lifecycle of effects and ensuring they are properly cleaned up when they are no longer needed.
This documentation provides a foundational understanding of the signal-based reactivity system. For more detailed information on specific functions or advanced use cases, please refer to the source code and any additional documentation.
@baseballyama
Copy link

I had Gemini 2.5 and ChatGPT o1 Pro explain the result of building with Vite using only the minimum necessary reactive code.

import * as $ from "../node_modules/svelte/src/internal/client/index.js";

const count = $.state(0);
const doubled = $.derived(() => {
  return $.get(count) * 2;
});

$.effect_root(() => {
  $.user_effect(() => {
    console.log(`${$.get(count)} * 2 = ${$.get(doubled)}`);
  });
});

@baseballyama
Copy link

Gemini 2.5 Pro


Svelte 5 Reactivity System: Developer Documentation

Introduction

This document explains the internal implementation and core concepts of Svelte 5's new reactivity system (commonly known as Runes), targeted towards Svelte core developers. Unlike the compiler-based reactivity of Svelte 4 and earlier, Svelte 5 employs a system based on more explicit and fine-grained reactivity primitives: Signals, Derived Signals, and Effects. This aims to improve runtime performance, code understandability, and enable more powerful features.

The provided codebase represents the core components of this new reactivity system.

Core Concepts

Svelte 5's reactivity is built upon three primary concepts:

  1. Signal (Source): The fundamental unit of reactive state. It holds a value and can notify dependents when that value changes. In the code, these are created by the source() function (internal) or the state() rune (public API).
  2. Derived Signal: A value computed based on the value(s) of one or more other Signals (either Sources or other Derived Signals). When a dependency's value changes, the Derived Signal's value is automatically recalculated (or marked as dirty). Created by the derived() function in the code.
  3. Effect: Executes side effects (like updating the DOM, logging to the console, fetching data, etc.) in reaction to changes in the value(s) of one or more Signals. Created by the effect() function or user_effect() (corresponding to the $effect rune) in the code.

These primitives form dependency relationships, constructing a reactive graph.

Signal (Source)

A Signal is the atomic state container forming the foundation of the reactive system.

  • Creation: source(initialValue) creates a basic Signal object. It includes properties like:
    • v: The current value.
    • equals: A function to compare value equality (defaults to ===). Used to detect changes.
    • reactions: A list of Derived Signals or Effects that depend on this Signal. They need to be notified when the value changes.
    • wv (Write Version): The global write version when the Signal was last updated.
    • rv (Read Version): The read version of the reaction that last read this Signal.
    • f: Bit flags indicating the Signal's state and characteristics.
  • state(initialValue): A higher-level API (rune) that internally calls source() and automatically registers it as a dependency of the current derived computation (if any) (push_derived_source).
  • Getting Value: Use the get(signal) function to retrieve the value. get registers the Signal as a dependency of the currently active reaction (Derived or Effect), if one exists.
  • Setting Value: (While no direct set function is in the code example) This is done by updating the Signal's v property and updating wv using increment_write_version(). This marks dependent reactions as dirty, scheduling them for recalculation or re-execution.

Derived Signal

A Derived Signal is a computed value derived from other Signals.

  • Creation: derived(computeFn) creates a Derived Signal object.
    • fn: The function to compute the value.
    • deps: A list of Signals this Derived Signal depends on.
    • effects: A list of Effects created within this Derived Signal (for nested reactivity).
    • parent: The scope (Effect or another Derived Signal) where this Derived Signal was created.
    • f: State flags. The DERIVED flag is always set.
  • Lazy Evaluation and Caching: The value of a Derived Signal is computed only when get() is called for the first time (or the first time after a dependency has changed). The result is cached in the v property and won't be recalculated unless a dependency changes.
  • Dependency Tracking: When get() is called during the execution of computeFn, the read Signal is automatically added to deps (logic within update_reaction).
  • State Management:
    • CLEAN: The value is up-to-date, and dependencies haven't changed.
    • DIRTY: The value is stale and needs recalculation the next time get() is called. Set when a dependent Source is directly changed.
    • MAYBE_DIRTY: A state indicating that one of the dependent Derived Signals might be MAYBE_DIRTY or DIRTY. Whether recalculation is actually needed is determined by recursively checking dependencies in check_dirtiness. This is an optimization to avoid unnecessary recalculations.
  • UNOWNED Flag: Typically, a Derived Signal is "owned" by the Effect (or component) that created it. UNOWNED indicates a Derived Signal that doesn't have a specific owner (Effect). This is relevant mainly when created outside an Effect scope or used at the top level. Unowned Derived Signals might be automatically cleaned up when no longer referenced (similar behavior to garbage collection).
  • DISCONNECTED Flag: Indicates that an UNOWNED Derived Signal is temporarily not referenced by any reaction. It needs to reconnect its dependencies if referenced again (within check_dirtiness).

Effect

Effects are responsible for executing side effects in response to Signal changes.

  • Creation:
    • effect(fn): Creates a standard Effect. Its execution is scheduled asynchronously.
    • user_effect(fn): Called when using the $effect rune within a Svelte component. Similar to effect(), but execution timing might be adjusted based on the component's mount state (defer logic).
    • effect_root(fn): Creates the root of an independent reactivity tree. Often used to manage reactivity for the entire application or specific isolated UI sections. Executes synchronously for the first time.
  • Dependency Tracking: Similar to Derived Signals, Signals get()'d during the execution of fn are registered as dependencies.
  • Execution Scheduling:
    • When a Source Signal changes, Effects depending on it are marked DIRTY or MAYBE_DIRTY and added to an execution queue (schedule_effect).
    • Actual execution happens asynchronously using microtasks (queueMicrotask) (flush_queued_root_effects). This allows batching multiple changes and skipping unnecessary intermediate state calculations or side effects.
    • flush_queued_effects processes the queued Effects, checks if they are dirty using check_dirtiness, and then calls update_effect to re-run them.
  • Cleanup: An Effect's fn can optionally return a cleanup function. This function is called before the Effect re-runs or when the Effect is destroyed (execute_effect_teardown). Used for tasks like removing event listeners or clearing timers.
  • Lifecycle:
    1. Creation: The Effect object is created with create_effect and linked to its parent Effect.
    2. Initial Run: fn is executed (synchronously for effect_root, asynchronously for effect), and dependencies are collected.
    3. Update: Marked dirty due to dependency changes, scheduled, and then re-executed via update_effect (after running the cleanup function, if any).
    4. Destruction: When destroy_effect is called, the cleanup function runs, the Effect removes itself from dependencies (remove_reactions), unlinks itself from the Effect tree (unlink_effect), and releases associated resources (like DOM nodes).
  • Effect Types (Flags):
    • EFFECT: A regular Effect.
    • ROOT_EFFECT: The root of a reactivity tree. The starting point for scheduling.
    • BLOCK_EFFECT, BRANCH_EFFECT: Special Effects generated by the Svelte compiler, primarily managing the lifecycle and DOM operations for block elements like {#if} and {#each}.
    • BOUNDARY_EFFECT: An Effect defining a boundary for error handling. Catches errors from child Effects (handle_error, propagate_error).

Reactivity Execution Model

  1. Read (get):
    • When get(signal) is called, it first checks if there's a current reaction (active_reaction) and if tracking is active (!untracking).
    • If so, it adds the Signal to the current reaction's dependencies (deps or new_deps). Versions (rv, read_version) help efficiently check if it's already registered within the same reaction.
    • If the Signal is Derived, check_dirtiness is called to ensure its value is current. If dirty, update_derived recalculates it.
    • Finally, it returns the Signal's current value (signal.v).
  2. Write (set):
    • (When modifying source value directly)
    • Update signal.v with the new value.
    • Update signal.wv using increment_write_version().
    • Mark each reaction (Derived or Effect) in signal.reactions as dirty (set_signal_status(reaction, DIRTY) or MAYBE_DIRTY).
    • If any Effects became dirty, call schedule_effect to schedule their execution.
  3. Update Cycle:
    • When the microtask queue is processed, flush_queued_root_effects is called.
    • process_effects starts from the scheduled root Effects and collects dirty Effects. During this, check_dirtiness for MAYBE_DIRTY Derived Signals is called, potentially triggering recalculations via update_derived.
    • flush_queued_effects loops through the collected dirty Effects, performs a final check with check_dirtiness, and calls update_effect to re-run the Effect function (fn).
    • update_reaction (Core logic shared by Derived and Effect):
      • Sets the current reaction (active_reaction), etc.
      • Executes fn. During this execution, get calls collect new dependencies into new_deps.
      • Compares old dependencies (deps) with new ones (new_deps), removes the reaction from dependencies that are no longer needed (remove_reactions), and adds the reaction to new dependencies.
      • For Effects, saves the cleanup function returned by fn into teardown.
  4. Error Handling: If an error occurs during Effect execution, handle_error is called. The error propagates up the parent Effect tree via propagate_error to the nearest ancestor Effect with the BOUNDARY_EFFECT flag. If not handled there, it's thrown globally.
  5. Cleanup: destroy_effect recursively destroys an Effect and its descendants. This includes running cleanup functions, removing dependencies, unlinking from the tree, and removing DOM nodes (if applicable).

Flags

The f property on each Signal and Effect object is a set of bit flags used to efficiently manage the object's state and type. Key flags include:

  • Type: DERIVED, EFFECT, BLOCK_EFFECT, BRANCH_EFFECT, ROOT_EFFECT, HEAD_EFFECT
  • State: CLEAN, DIRTY, MAYBE_DIRTY
  • Ownership/Connection: UNOWNED, DISCONNECTED
  • Lifecycle: DESTROYED, INERT, EFFECT_RAN
  • Others: BOUNDARY_EFFECT (error handling), EFFECT_HAS_DERIVED (optimization)

These flags are efficiently set, unset, and checked using bitwise operations (|, &, ^) (e.g., set_signal_status).

Context and Ownership

  • active_reaction / active_effect: Global (module-scoped) variables tracking the currently executing reaction (Derived or Effect) or Effect. Essential for registering dependencies correctly when get is called.
  • Effect Tree: Effects form a tree structure with parent-child relationships (parent, first, last, next, prev). This allows hierarchical management of scheduling (ROOT_EFFECT), error handling (BOUNDARY_EFFECT), and cleanup (destroy_effect_children).
  • component_context: Holds the Svelte component instance associated with an Effect or Derived Signal. This links component-specific behavior (like lifecycle) with the reactivity system.
  • UNOWNED: Derived Signals that don't belong to the Effect tree. Their lifecycle tends to be managed by a mechanism resembling reference counting.

Optimizations

  • MAYBE_DIRTY: When a Signal changes, direct dependents become DIRTY, but further dependents become MAYBE_DIRTY. This delays expensive recalculations until check_dirtiness confirms if the value actually changed.
  • Versioning (wv, rv, read_version, write_version): Tracking Signal reads and writes with version numbers streamlines check_dirtiness and dependency updates.
  • skip_reaction: A flag to temporarily skip dependency tracking and updates under specific circumstances (e.g., an Unowned Derived read outside an Effect scope).
  • untracking: The internal flag/function corresponding to the untrack() API. Prevents dependency registration for get calls within a specified function's execution.

This document provides a high-level overview of the concepts and internal workings of the Svelte 5 reactivity system as inferred from the provided codebase. The actual system involves more details and edge cases, but this explanation should aid in understanding the core mechanics.

@baseballyama
Copy link

ChatGPT o1 pro


Basic Elements

  1. state(...) / source(...):
    Holds a value and tracks dependent reactions. Any write triggers “dirty” flags on dependent reactions.

  2. derived(...):
    A signal that recalculates its value based on other signals. Automatically re-evaluates when dependencies change.

  3. effect(...) / effect_root(...):
    A reaction for executing side effects. Runs whenever any accessed signals change. effect_root is treated as a root and is scheduled first.

  4. user_effect(...):
    A wrapper for user-facing effect code, allowing finer control over the timing and context of execution.

Reactivity Flow

  1. Reading (get):
    When a reaction is active, reading a signal adds it to that reaction’s dependency list.

  2. Writing (set/increment):
    Changing a signal marks reactions that depend on it as “dirty,” triggering updates.

  3. Recalculation (update_reaction / check_dirtiness):
    Dirty reactions are scheduled. Dependencies are checked, and if necessary, the reaction re-runs with new values.

  4. Scheduling (schedule_effect, flush_queued_root_effects):
    Updates and recalculations are batched in a microtask. Root and nested effects are processed so only necessary parts re-run.

Key Flags and Status

  • DIRTY / CLEAN / MAYBE_DIRTY: Tracks whether a reaction needs recalculation.
  • DERIVED: Indicates a derived signal.
  • EFFECT / ROOT_EFFECT / BRANCH_EFFECT: Classifies reaction types.
  • UNOWNED / DISCONNECTED: Indicates detached or “floating” states in the dependency graph.

Error Handling and Boundaries

  • BOUNDARY_EFFECT: Marks an effect that can catch errors within its scope.
  • handle_error / propagate_error: Pass exceptions upward through boundary effects until handled or ultimately thrown.

Summary

Svelte5’s reactivity uses signals (with stored values) and reactions (effects, derived signals) to form a dependency graph. When values update, only affected parts re-run, aided by efficient scheduling in a microtask queue. Internal flags manage state transitions, while boundaries and error-handling ensure robust behavior under edge cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment