structura.js

repository·master·Indexed 19 days ago

https://github.com/giusepperaso/structura.js

A high-performance, lightweight TypeScript library for creating immutable states using a mutable syntax. It utilizes structural sharing and compile-time immutability via TypeScript to optimize performance, claiming speeds up to 10x faster than Immer.js. The library supports circular and multiple references, asynchronous state updates via asyncProduce, and specialized helpers like safeProduce for enhanced type safety.

Tokens
12K
Snippets
49
Records
57
Agent score
64%

What's inside structura.js

  1. Overview of Structura.js

    master

    Structura.js is a high-performance, lightweight TypeScript library designed for creating immutable states using a mutable syntax. It leverages the concept of structural sharing to ensure efficiency.

    Key features and advantages include:

    • High Performance: Up to ~10x faster than Immer.js and often faster than Immutable.js.
    • Compile-time Immutability: Unlike libraries that use Object.freeze at runtime (which can be slow for nested objects), Structura.js leverages TypeScript to freeze objects at compile time.
    • Advanced Reference Support: Supports circular and multiple references.
    • Flexible Producer API: Can return and modify the draft simultaneously, and offers flexibility in the return type of the producer.
    • Complex Operations: Supports transpositions and moves of portions of the draft.
    • Zero-toggle configuration: Most features are enabled by default.
  2. Why use Structura.js for immutable state management

    master

    Structura.js is designed to provide a high-performance alternative to common immutable state management patterns. It aims to bridge the gap between the readability of Immer.js and the performance of manual object spreading or Immutable.js.

    Key advantages include:

    • Performance: Offers syntax similar to Immer.js but can be up to ~10x faster (and sometimes faster than Immutable.js).
    • Readability: Uses a mutable-style syntax via a produce function, making it easy to understand and write even for complex state updates.
    • Size: Smaller bundle size compared to heavy alternatives.
    • Compile-time Freezing: Objects are frozen at compile-time rather than runtime.
    • Edge Case Handling: Specifically optimized to handle edge cases that other libraries struggle with.
  3. Patch compatibility and standard JSON Patches

    master

    By default, Structura patches do not comply with RFC 6902. If you need to use patches with other libraries or languages, you have two options:

    1. Enable Standard Patches: Turn on the setting for standard patches (see settings.html#enable-standard-patches).
    2. Use a Converter: Use the converter available in helpers.html#convertpatchestostandard to transform Structura-generated patches into standard JSON Patches.
  4. Manage multiple references to the same object

    master

    When an object is referenced multiple times in your state, Structura applies the immutability algorithm to all traversed parents. However, there are important behaviors to note regarding how these references are updated:

    1. Traversed Parents: If you modify a shared object via one path, other paths will reflect the change if they are also traversed.
    2. Access Requirement: If a path to a shared object is never accessed/traversed within the producer, that specific reference in the new state will remain identical to the old state (it won't be updated to reflect the change in the shared object).
    3. Ensuring Updates: To ensure all references to a shared object are correctly updated in the new state, you may need to explicitly reassign the reference (e.g., draft.key = draft.key) if the automatic traversal doesn't cover all paths.
    const array = [1]
    const state = { test1: array, test2: array }
    
    // Works: both paths are modified
    const newState1 = produce(state, (draft) => {
        draft.test1.push(1)
        draft.test2.push(1)
    })
    
    // Works: explicit reassignment ensures test1 reflects changes
    const newState4 = produce(state, (draft) => {
        draft.test2.push(1)
        draft.test1 = draft.test1;
    })
    
    // Does NOT work as expected: test2 is never accessed, so it remains the old reference
    const newState5 = produce(state, (draft) => {
        draft.test1.push(1)
    })
  5. Freeze objects at compile time with Structura

    master

    Unlike libraries that use Object.freeze at runtime, Structura performs freezing at compile time using TypeScript's readonly flags. This approach provides zero performance overhead during execution. When using produce, the resulting state is automatically frozen at the type level, preventing mutations from compiling.

    // newState gets automatically frozen via produce
    const newState = produce(state, (draft) => {
        draft.push(4);
    })
    
    newState.push(5) // DOESN'T COMPILE
  6. Handle circular references automatically

    master

    Structura automatically handles circular references within your state object. You do not need to manually freeze objects to prevent infinite loops during the production process.

    const state: any = { test1: [1], test2: [2], test3: null }
    state.test3 = state
    
    // This works without going infinite
    const newState = produce(state, (draft) => {
        draft.test3.test1.push(1)
    })
  7. When to use Structura.js instead of Immer

    master

    Structura.js is a better choice than Immer in the following scenarios:

    • Performance & Scale: When immutable state updates are becoming a bottleneck or when dealing with very large and complex state trees.
    • Resource Constraints: In serverless or cloud environments where minimizing resource usage is critical.
    • Complex State Structures: When your state contains circular references or multiple references to the same object.
    • Flexible API Requirements:
      • When you want to avoid being limited by the return type of the producer.
      • When you need to modify a draft and return a specific portion of it within the same producer function.
    • Simplicity & Customization:
      • When you prefer a library without a large set of features you might not need.
      • When you want a small, easy-to-reason-about codebase that is simple to fork and adapt for specific use cases.
  8. Avoid type errors in producers using safeProduce

    master

    By default, Structura's produce function allows producers to return any type. This can lead to accidental errors where a producer returns a primitive (like a number) instead of the intended state object, especially if you forget to use curly braces in an arrow function.

    To prevent this, you have two options:

    1. Explicit Generics: Manually declare the generic parameters for produce to enforce the expected return type.
    2. Use safeProduce: Use the safeProduce helper, which enforces that the return type of the producer must match the type of the initial state. This is the recommended way to ensure type safety without manual generic declarations.

    safeProduceWithPatches behaves identically to safeProduce but supports patch generation (similar to produceWithPatches).

    const state = { test: 1 }
    
    type T = { test: number }
    
    // Option 1: Explicit Generics (prevents returning the wrong type)
    // This will error if the producer returns something other than T
    const result = produce<T, T>(state, (draft) => draft.test = 2)
    
    // Option 2: safeProduce (automatically enforces state type == result type)
    // This will error because the producer returns a number instead of the state object
    const result = safeProduce(state, (draft) => draft.test = 2)
  9. Perform transpositions (reassigning keys)

    master

    You can reassign keys of sub-objects within the draft (transpositions), and Structura will handle the immutability correctly.

    Warning: This does not work if you attempt to assign a draft-proxied object to a new object that is external to the draft. Doing so will result in the new object retaining a proxy attached via its property, which is likely unintended behavior.

    const state = [[1], [2]]
    
    // Works: swapping elements within the draft
    const newState1 = produce(state, (draft) => {
        const first = draft[0]
        draft[0] = draft[1]
        draft[1] = first
    })
    
    // Does NOT work well: assigning a draft object to an external object
    // results in the new object retaining a proxy attached via its property
    const newState3 = produce(state, (draft) => {
        const first = draft[0]
        const newObj = { prop: first }
        draft.push(newObj as any)
    })
  10. Extend draftable types for custom classes

    master

    If a custom class is not recognized as draftable, you can extend DraftableTypes by adding its [Symbol.toStringTag] value.

    import { DraftableTypes } from "structurajs";
    
    class MyClass {
      get [Symbol.toStringTag]() {
        return 'MyClass';
      }
    }
    
    DraftableTypes.push("[object MyClass]");