DEV Community

mogera551
mogera551

Posted on

No Boilerplate, State Hooks, or Virtual DOM: Structural Paths Usher in a New Era of Frontend Development

Basic Concept
This framework considers UI and state as fundamentally different representations of the same information, treating the structural path as the only reliable source of truth for both UI and state and elevating it to a first-class object. By leveraging existing technologies such as Proxy, it aims to solve many challenges faced in modern web frontend development.

A “structural path” here refers to a full path that points to the structural position of data. To express abstract references, wildcards are used for list elements.
Example:
user = { name: "Alice" };
=> user.name
users = [ { name: "Alice" }, { name: "Bob" } ]
=> users.*.name // Points to Alice or Bob—not “all,” but specifically indicates the data position.


Problems Solved

  • Fundamental Solution to the Complexity of State Management
    By making the structural path the single source of truth, consistency between UI and state is guaranteed, dramatically reducing complex state management logic. This is especially beneficial for large-scale application development.

  • Elimination of UI-State Synchronization Issues
    By closely tying UI and state via the structural path, synchronization mismatches between the two are fundamentally resolved. Although the link is tight, UI and state are associated by contract (the structural path) and, in line with the Dependency Inversion Principle, are considered loosely coupled.

  • Reduction of State Hooks, Boilerplate, and Improved Developer Experience
    Treating the structural path as a first-class object makes data referencing and updates more intuitive, reducing redundant code. By leveraging Proxy’s set trap, state changes can be detected and trigger updates, making state hooks unnecessary. This allows developers to focus more on essential logic.

  • No Need for Virtual DOM and Potential Performance Gains
    When structural paths directly bind to UI elements, intermediate layers such as a virtual DOM become unnecessary, enabling more efficient DOM operations. Because UI and state are connected 1:1 via structural paths, changes to the state can pinpoint-update the relevant UI, and vice versa. This fine-grained update per structural path promises significant performance benefits.

  • Easier Debugging, Reasoning, and Predictability
    Clearly defined structural paths make it easier to track application state changes, facilitating debugging and making code behavior more predictable.

  • Reduced Cognitive Load
    With a single concept (structural path) unifying UI and state, there’s less for developers to learn, reducing both the learning curve and cognitive burden. When UI and state are described within a single component file, using the same structural path for both eliminates context switching, greatly reducing cognitive load. Additionally, using structural paths as a common language in team development significantly eases team-wide cognitive load.

  • Enhanced Declarative Expression
    Structural paths clarify which data structure the UI represents, further strengthening the declarative nature of UI descriptions. Especially with getters, abstract structural paths using wildcards can be used, enabling even more declarative definitions.


Implementation Outline

  • Data expressed in the UI (conditionals, loops, text interpolation, attribute binding) are specified using structural paths.

  • State is managed by a class, with state as properties, derived state as getters, and state updates as methods.

  • Referencing and updating in derived state definitions and update methods use structural paths.

  • The state management class is extended with Proxy; the get trap resolves structural paths and the set trap detects update triggers.

  • UI templates and state classes are packaged in a single HTML file and operate as components.

  • The UI template is parsed to obtain UI elements targeted by structural paths. When expanding a loop, the block inside is similarly parsed, and the UI elements targeted by structural paths are obtained. The acquired structural paths and UI are managed as data binding information.

  • When an update trigger occurs, the update target is determined based on the data binding information.

  • When expanding a loop in the UI, a loop context is created and stacked.

  • The loop context is associated with any data binding or event handler within the loop.

  • When referencing data bindings or executing event handlers, the corresponding loop context is referred to for structural path resolution.

  • Even in nested loops, loop contexts are stacked, so any desired loop context can be referenced.

  • Parent and child components reference parts of the parent’s state, associating the parent’s structural path with the child’s and resolving references and updates through path conversion.

  • By making dependencies between parent and child components a contract via the structural path, the Dependency Inversion Principle is followed, enabling loose coupling.

  • The introduction of parent-child components enables divide-and-conquer and greatly alleviates scalability concerns.


Implementation Example

<template>
  <!-- UI -->
  <ul>
  {{ for:users }}
    <li>
      {{ users.*.name }}, {{ users.*.ucName }}
      {{ if:users.*.isInactive }}
        <button data-bind="onclick:activate">activate</button>
      {{ endif: }}
    </li>
  {{ endfor: }}
  </ul>
</template>

<script type="module">
export default class {
  // State: users.*.name indicates Alice, Bob
  users = [ { name:"Alice", active:true }, { name:"Bob", active:false } ];

  // Derived state: users.*.ucName indicates ALICE, BOB
  // Within a loop, the structural path below can be resolved by the loop context
  get "users.*.ucName"() {
    return this["users.*.name"].toUpperCase();
  }

  // Derived state: users.*.isInactive indicates true or false
  // Within a loop, the structural path below can be resolved by the loop context
  get "users.*.isInactive"() {
    return !this["users.*.active"];
  }

  // State update: updates users.*.active via structural path
  // Within a loop, only the users.*.active matching the loop context is changed, and the corresponding UI is updated
  activate() {
    this["users.*.active"] = true;
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Implementation Example (Parent/Child)

Parent Component

<template>
  <!-- UI -->
  <ul>
  {{ for:users }}
    <user-comp data-bind="state.user: users.*"></user-comp>
  {{ endfor: }}
  </ul>
</template>

<script type="module">
export default class {
  users = [ { name:"Alice", active:true }, { name:"Bob", active:false } ];
  get "users.*.ucName"() {
    return this["users.*.name"].toUpperCase();
  }
  get "users.*.isInactive"() {
    return !this["users.*.active"];
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Child Component

<template>
  <li>
  {{ user.name }}, {{ user.ucName }}
  {{ if:user.isInactive }}
    <button data-bind="onclick:activate">activate</button>
  {{ endif: }}
  </li>
</template>

<script type="module">
export default class {
  user = {};
  activate() {
    this["user.active"] = true;
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

Constraints

  • Since the structural path is the only reliable source, using objects as arrays indexed by string keys (like associative arrays) is inappropriate; associative arrays should be treated as objects.
  • UI and state must use the same data structure. However, by utilizing array filter functions, converting to key-value arrays, and using derived state via getters, most UI structures can be represented.
  • Due to the single-source-of-truth principle for structural paths, nested loops must always be elements of the original loop. Nested loops with unrelated structural paths cannot be created.
  • Structural paths for large data sets become lengthy, but they are self-descriptive, finite within a component, follow certain rules, and are easy to analyze. This also enables AI suggestions and IDE support (with separate implementation).
  • By splitting components and delegating part of the state, structural paths can be localized and shortened.
  • To uphold the “only reliable source” principle, introducing aliases, scoped variables, or shorthand notations is considered inappropriate.

Top comments (0)