Skip to content
zalky edited this page May 1, 2023 · 11 revisions

Reflet

Clojars Project

Reflet is a set of tools for building Re-frame + React based web apps with graph and non-graph data models.

At the core of Reflet is a single macro, with-ref, that generates references to any kind of thing, and also transparently manages the lifecycles of those things throughout your application. This leads to highly extensible components, and excellent APIs.

Besides with-ref, Reflet also provides:

  1. Performant multi-model DB with graph queries and mutations
  2. Data-driven descriptions: a new kind of polymorphic query
  3. Simple but powerful hierarchical FSMs
  4. JS and DOM interop utilities
  5. Novel API-driven visual debugging of complex apps (don't sleep on this!)

And more!

Reflet aims to be a natural progression on top of Re-frame to support complex, data-driven requirements. In that sense, it is both easy to learn, but powerful. You could say it's sort of like Re-frame++ (or Fulcro for Re-frame). Its main design goals are:

  1. A la carte feature set: it is not a "framework", so use as much or as little of it as you want
  2. Power up existing Re-frame applications: iterative, minimal approach to integration, so big re-writes can be avoided
  3. Graph and non-graph data models can be mixed freely with a single source of truth: a Clojure map
  4. Entity references connect things together: this encourages excellent, pluggable APIs
  5. Makes very few assumptions about your application boundaries
  6. Performance and stability: Reflet has already been deployed in complex, data-driven production applications for 4+ years (e.g. in Bioinformatics, Business analytics... )

Installation

At minimum include the following in your deps.edn:

{:deps {io.zalky/reflet {:mvn/version "0.3.0-rc1"}}}

Or project.clj:

[io.zalky/reflet "0.3.0-rc1"]

Additionally, React is considered a peer dependency, so you will have to ensure that it is available. The same approach you would use to provide React for Reagent or Re-frame will also work for Reflet.

See the additional notes on how to configure the debugger for development.

Resources

Reflet builds on top of the concepts and design patterns of Re-frame, and these resources assume a working knowledge of Re-frame basics. If you are not familiar with Re-frame, check out that documentation first, it is very good. Otherwise:

  1. Rationale and Overview
  2. Quick Start
  3. Configuration
  4. Features
  5. Example Client
  6. General Development Tips

Rationale and Overview

Reflet can be broadly divided into 6 feature sets.

References

At the core of Reflet is a single macro: with-ref. If you use nothing else, this is it. It lets you name things via unique references, and parameterize components so that they are easily reusable. Sprinkled throughout your component hierarchy, these references become the connective tissue that binds all the facets of your application together:

  1. Domain app state
  2. Component state
  3. Events, queries, and FSMs
  4. Mutable JS object management
  5. DOM elements
  6. Miscellaneous ids for things like focus management

Anywhere you need an id, a name, or a reference to something, with-ref is your friend. This is true not just in some esoteric sense of a "unified approach", but because how the specific behaviour of with-ref leads to excellent APIs.

Multi-Model DB

Reflet also introduces a multi-model, reactive graph db. Or rather, it brings a graph data model to your existing Re-frame db (a simple Clojure map) in a way that supports high performance graph queries and mutations. But your graph data can happily co-exist along side non-graph data, not just in the db, but in your event handlers and components. Anywhere in your app, you are free to choose one or the other data model, or a combination of both, depending on your needs. This is especially useful if you already have a complex app that could benefit from a graph data model, but you want a minimalist approach that does not require an extensive re-write.

Graph Queries

If you've used Datomic, Datascript, EQL or GraphQL, the semantics of Reflet's graph queries will be familiar. In fact, the Reflet query DSL is a variant of EQL grammar, with union queries replaced by the Reflet's polymorphic descriptions. A differential event sourcing algorithm drives fast, reactive graph queries that only update when necessary. Tens of thousands of query instances can exist at once, such that the browser's ability to render DOM nodes to the window is often a more likely bottleneck.

Polymorphic Descriptions

EQL, GraphQL, and their variants provide a level of query polymorphism through a feature called union queries. Reflet provides a new variant of polymorphic query called descriptions that enhances expressiveness, extensibility, and re-use, especially when the data is deeper and more complex.

Importantly, descriptions are defined outside of the queries in which they will eventually participate. Each description is defined for a particular context, and can reference other contexts in its definition. What's more, everything from the polymorphic type hierarchies to the descriptions themselves can be defined as run-time data, providing a more data-driven experience. This is especially nice if your types and taxonomies reside in a remote ontology, schema, or knowledge representation layer.

Finite State Machines

This document explains how Re-frame itself can be seen as a machine that transitions through states, though not one that is formally specified as an FSM. Often, Re-frame's transition logic is spread throughout many event handlers, which can make complex behaviours difficult to reason about. Reflet provides a hierarchical FSM DSL, that allows these behaviours to be modelled in a declarative, concise way. Critically, FSMs can be used as inputs to other FSMs to create nested models that are more like Behaviour Trees. This allows you to separate out the logic of your application from the implementation, and in some cases even eliminate the implementation entirely.

Mutable State

Two other places Reflet helps are:

  1. Interop with mutable Javascript libraries
  2. Direct DOM access and manipulation

Working with mutable Javascript libraries or accessing the DOM directly is inevitable. When this breaks the model of the UI as a materialized, pure function of immutable state, the tried-and-true inner/outer component pattern can help. However, the traditional approach to this pattern can hamper extensiblity. Reflet's unified with-ref model streamlines this pattern and helps bring some of the same API and application design benefits to the mutable state story.

Debugger

Finally, Reflet provides a novel visual debugger that is specifically designed to inspect complex, data driven apps. While debuggers like re-frame-10x are indispensable, they provide a global lens into the behaviour of your application: everything all at once. With a complex web app, it can sometimes be a challenge to reduce the signal to noise ratio.

More often you want to drill directly down into a subset of components, their data, and interdependencies. Since references are the connective tissue that binds everything together in Reflet, with-ref becomes the perfect entry point for exactly this kind of exploration. The Reflet visual debugger renders an overlay of your with-ref API directly onto your components in the browser. You can then explore your application via a set of four lenses: DB, EVENTS, QUERY, and FSM. References are everywhere in Reflet, so follow your nose.

Oh, and of course the debugger was written entirely using Reflet.

TODO

The following features have been mapped out, but are currently in the backlog until demand for them materializes:

  1. Canonicalization of unique references

Getting Help

You can either submit an issue here on Github, or alternatively tag me (@zalky) with your question in the #re-frame channel on the Clojurians slack.

Contribution Acknowledgements

Special thanks to Inge Solvoll.

License

Reflet is distributed under the terms of the Apache License 2.0.