use-mutable-source

website·Indexed 29 days ago

https://paol-imi.github.io/use-mutable-source/

A React library designed to manage the lifecycle, state derivations, and mutations of a mutable source. It provides tools like useSource and usePureSource to enable components to safely and efficiently use mutable sources and derive state from them, facilitating the integration of external libraries such as GSAP.

Tokens
5.7K
Snippets
26
Records
30
Agent score
75%

What's inside use-mutable-source

  1. Overview of use-mutable-source

    use-mutable-source is a minimal library (approx. 600 bytes) designed to help developers safely manage local state using mutable objects or external non-React libraries (e.g., state machines, drag-and-drop libraries, or legacy DOM libraries) within React components. It abstracts the complexity of React's strict rules regarding mutations during render, ensuring that integrations are concurrent-safe and compatible with React 18's <StrictMode> and Fast Refresh, preventing issues like 'tearing'.
  2. Understand the core concepts of use-mutable-source

    The library manages the lifecycle, state derivations, and mutations of a mutable source using three primary functions:

    • init(): Defines how the source is generated.
    • getSnapshot(): Defines how state is derived from the source.
    • subscribe(): Defines how to subscribe to mutation events.
  3. Choose between useSource and usePureSource hooks

    The use-mutable-source library provides two main hooks to manage the lifecycle, state derivations, and mutations of a mutable source. The choice depends on whether the source initialization involves side-effects:

    • Use useSource if the source initialization has side-effects, such as accessing the DOM or a React Ref.
    • Use usePureSource if the source initialization has no side-effects.
  4. Derive immutable state using useSnapshot

    Because reading mutable objects directly during render is forbidden in React, useSnapshot is used to derive an immutable snapshot from the source.

    • Snapshot Function: A pure function that takes the source and returns a snapshot. Since the source is null during the first render, a defaultSnapshot must be provided.
    • Subscription: A function that subscribes to events that change the source and returns an unsubscribe function.
    • Dynamic Snapshots: If the snapshot derivation depends on external variables, provide a dependency list as the second argument to useSnapshot (before the subscription function).
    const snapshot = useSnapshot(
      // 1. Derive snapshot (handle null source for first render)
      (source) => (source ? getSnapshot(source) : defaultSnapshot),
      // 2. Subscribe to changes
      (source, onChange) => {
        subscribe(source, onChange);
        return () => unsubscribe(source, onChange);
      }
    );
    
    // Example with dynamic dependencies for the snapshot
    const snapshot = useSnapshot(
      (source) => (source ? getSnapshot(source, dep1, dep2) : defaultSnapshot),
      [dep1, dep2], // Dependency list for getSnapshot
      subscribe
    );
    
  5. Manage dependencies between non-Pure sources

    Non-Pure sources are more flexible. You can use classic composition regardless of order, as side-effects are permitted within the initialization function of a non-Pure source. Alternatively, use useEffect for dynamic additions and removals.
    // ✅ Valid: Classic composition
    const [, getItem] = useSource(() => [new Item()]);
    const [, getContainer] = useSource(
      () => [new Container(getItem())],
      [getItem]
    );
    
    // ✅ Valid: Side-effect inside non-Pure init function
    const [, getContainer] = useSource(() => [new Container()]);
    const [, getItem] = useSource(() => {
      const item = new Item();
      const container = getContainer();
      container.add(item);
      return [item];
    }, [getContainer]);
  6. Subscribe to source changes in useSnapshot

    To keep snapshots up to date, useSnapshot requires a subscribe function. This function must register a listener for events that change the source using the provided onChange callback and must return an unsubscribe function. If the subscription depends on external variables, a dependency list can be passed as the third argument to trigger resubscription.
    const snapshot = useSnapshot(
      getSnapshot,
      (source, onChange) => {
        subscribe(source, onChange);
        return () => unsubscribe(source, onChange);
      }
    );
    
    // Example with dynamic subscription dependencies
    const snapshot = useSnapshot(
      getSnapshot,
      (source, onChange) => {
        subscribe(source, onChange, dep1, dep2);
        return () => unsubscribe(source, onChange, dep1, dep2);
      },
      [dep1, dep2]
    );
  7. Use implicit memoization in hooks

    Hooks in use-mutable-source treat memoization as implicit. Instead of wrapping functions in useCallback, you can inline them directly within the hook and provide a dependency list immediately following the function definition. If no dependency list is provided, the function is treated as stable (equivalent to an empty dependency list []).
    // Example of implicit memoization for a source and its snapshot/subscription
    usePureSource(
      () => new Source(dep1, dep2),
      [dep1, dep2]
    );
    
    useSnapshot(
      (source) => getSnapshot(source, dep3, dep4),
      [dep3, dep4],
      (source, onChange) => subscribe(source, onChange, dep5, dep6),
      [dep5, dep6]
    );
  8. Combine Contracts and Concurrent Mode

    For maximum performance, use the atomic version of the contract API. In this configuration, the contract is passed before the snapshot derivation function. All functions must be stable.
    import { usePureSource } from 'use-mutable-source/with-contract/atomic';
    
    const [snapshot, source] = usePureSource(
      () => new Source(),
      (source, onChange) => void subscribe(source, onChange),
      (source) => getSnapshot(source)
    );
  9. Derive state from a source using useSnapshot

    Since reading mutable objects during render is forbidden in React, useSnapshot is used to derive an immutable snapshot from a source. It requires a pure function to generate the snapshot and a subscription function to notify the hook of changes.

    useSnapshot compares the new snapshot with the current one after each render; if they differ, it forces a re-render. If the snapshot is dynamic, you can provide a dependency list immediately after the snapshot derivation function.

    const snapshot = useSnapshot(
      // Derives the snapshot
      (source) => getSnapshot(source),
      // Subscribes to changes
      subscribeToChanges
    );
    
    // Example with dynamic dependencies for the snapshot
    const snapshot = useSnapshot(
      (source) => getSnapshot(source, dep1, dep2),
      [dep1, dep2],
      subscribe
    );
  10. Initialize a mutable source with useSource

    The useSource hook allows you to define a mutable source that is lazily initialized when first accessed. It returns a tuple containing a useSnapshot function (to derive state) and a getSource function (to access the mutable object).

    • Initialization: Provide a function that returns the source.
    • Dependencies: An optional second parameter (dependency list) will cause the source to be recreated if any dependency changes.
    • Cleanup: You can provide a destroy function as the second element of the initialization tuple to clean up the source when its lifecycle ends.
    // Basic initialization
    const [useSnapshot, getSource] = useSource(() => [new Source()]);
    
    // Initialization with dependencies
    const [useSnapshot, getSource] = useSource(
      () => [new Source(dep1, dep2)],
      [dep1, dep2]
    );
    
    // Initialization with a destroy function
    const [useSnapshot, getSource] = useSource(() => [
      new Source(),
      (source) => source.destroy(),
    ]);
  11. Manage dependencies between Pure sources

    When relating two Pure sources, use classic composition where the dependency is defined before the dependent source. The dependent source must include the dependency in its dependency array. Avoid performing mutations (side-effects) inside the initialization function of a Pure source to insert an item into another source.
    // ✅ Valid: Item is defined before Container
    const [, item] = usePureSource(() => new Item());
    const [, container] = usePureSource(
      () => new Container(item),
      [item]
    );
    
    // ❌ Invalid: Side-effect inside init function
    const [, container] = usePureSource(() => new Container());
    const [, item] = usePureSource(() => {
      const item = new Item();
      container.add(item); // Side-effect!
      return item;
    }, [container]);
  12. Implement atomic snapshots with contracts

    For cases requiring an atomic equivalent, use-mutable-source/with-contract/atomic allows you to define the source, the subscription contract, and the snapshot derivation in a single useSource call. This returns the unique snapshot directly instead of a useSnapshot hook. All provided functions must be stable and cannot be dynamic.
    import { useSource } from 'use-mutable-source/with-contract/atomic';
    
    const [snapshot, getSource] = useSource(
      // 1. Defines the source
      () => [new Source()],
      // 2. Subscribes to changes and returns the unsubscribe function
      (source, onChange) => subscribe(source, onChange),
      // 3. Derives the snapshot
      (source) => (source ? getSnapshot(source) : defaultSnapshot)
    );