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

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