Signals, computed properties and other reactivity patterns in Hologram

Hi Lucas,

Thank you for sharing! The ExSheet API looks nice and clean. I can see how the explicit pipeline approach with selective memoization works well for spreadsheet-like computation graphs.

I think these are kind of two different use cases that probably have different optimal solutions, and your approach makes sense for what you’re building.

Different problems, different constraints

Component-scoped computed properties vs. general computation DAGs have fundamentally different needs. Spreadsheet DAG computations often need external I/O, side effects, and selective execution - exactly what your library handles. UI component derived state, on the other hand, should be pure functions, synchronous, always safe to recompute, deterministic, and locally scoped. Your solution is more flexible and general-purpose, which fits your use case.

Why tight framework integration makes sense for UI components

The architecture of modern web frameworks has kind of converged to the same patterns: component-based, composition pattern, and reactive/signal patterns. Users currently expect these reactive/signal patterns to be provided by the framework, and that makes sense because the reactivity system needs to be tightly coupled with the rendering engine. Frameworks can optimize the entire update cycle when they control reactivity.

When reactivity is built-in you get one consistent mental model throughout the app and possibly better tooling/debugging/documentation because the framework can understand the reactive state.

Hologram-specific considerations

If Hologram used a separate library for component computations:

  1. Transpilation overhead: The computation machinery would have to be transpiled from Elixir to JS. There’s always some overhead (boxed types, proxies for function calls, data cloning due to immutability, etc.). For critical performance paths that run on each render, it’s better to implement things by hand in JS. This is where compile-time analysis comes in - it can extract dependency information, build the computation graph, determine topological sort order, and generate optimized metadata at compile-time, then feed all of this to hand-written JavaScript code that executes efficiently at runtime without the transpilation overhead.

  2. Framework integration: The macro pattern helps take business logic out of templates. With a separate library, templates would need something like {MyComputedState.get(@computed_state, :full_name)} rather than just {@full_name}.

  3. Orchestration: Each action would need to imperatively update the computation state whenever dependencies change. When using macros, that orchestration happens automatically. This becomes even more valuable when Hologram introduces reactive queries from local-first data sources.

  4. Performance optimizations: When the framework understands the reactivity model, it can apply sophisticated optimizations like offloading computations to micro-tasks, batching updates, or scheduling work based on priority. These kinds of optimizations are much harder when reactivity is external to the framework and the computation is just an opaque function call.

On automatic vs. explicit dependencies

Both approaches are DSLs with different trade-offs:

# automatic dependency tracking
derived :full_name do
  "#{first_name} #{last_name}"
end

# explicit version
|> cell(:full_name, 
    inputs: [:first_name, :last_name], 
    compute: fn (first_name, last_name) -> "#{first_name} #{last_name}" end, 
    cache: true)

The explicit version gives you fine-grained control over caching and execution (great for general computation graphs where some cells may need I/O or shouldn’t be cached). The automatic version reduces boilerplate and eliminates the “missing dependency” bug class. Since Elixir provides compile-time analysis through macros, and Hologram has the call graph available, automatic tracking leverages those strengths.

For UI components where you typically want everything memoized by default, opt-out memoization creates a “pit of success” - good performance without profiling every decision.

Question for you

Can you explain a little bit more about this problem? “It went in the way of explicitness and prevented using function captures without adding a lot of workaround code.” I’d love to understand what specific challenges you encountered - it would be helpful to think through those edge cases.


So as you noticed yourself, these are different use cases. Your ExSheet library solves a more general problem with appropriate flexibility, while component-derived state has narrower constraints that benefit from tighter framework integration. Both approaches make sense in their respective contexts!

Thanks again for sharing your work!