Fulcro is a full-stack application programming system centered around graph-based UI and data management, working in conjunction with Pathom for server-side data processing.
- Graphs and Graph Queries: Excellent way to generalize data models
- UI trees as directed graphs: Easily "fed" from graph queries
- User operations as transactions: Modeled as data structures that look like function calls
- Arbitrary server graphs: Need database-style normalization
- UI tree repetition: Same data appears in multiple places
- Local manipulation: Requires de-duplication of graph data
- Composition first: Seamless composition is key to software sustainability
- Yes, supported: Use Sablono library
- CLJS-only: No server-side rendering support
- Fulcro preference: Functions/macros for isomorphic rendering
- Graph-centric CQRS: Mutations as "commands" queued like Redux events
- Enhanced features: Auto-normalization, centralized transaction processing
- UI State Machines: Actor model with runtime role assignment
- Not core concern: Websocket remote makes Meteor-like experience possible
- Server-side focus: Main difficulty is server-side subscription management
- Custom remotes: Adapt to existing GraphQL infrastructure
- CLJC support: Runs in headless JS or Java VM environments
- DOM independence: Server DOM methods work without JS engine
- Isomorphic: Same code runs client and server
- No database requirements: Works with any backend or no backend
- Schema flexibility: Pathom reshapes any schema to match UI needs
- Storage agnostic: Backend structure doesn't matter for client
[:person/name {:person/address [:address/street]}]
Returns:
{:person/name "Joe"
:person/address {:address/street "111 Main St."}}
To-many results:
{:person/name "Joe"
:person/address [{:address/street "111 Main St."}
{:address/street "345 Center Ave."}]}
- Simple structure: Tables with IDs, edges as vectors
[TABLE ID]
- Idents: Tuples of table and ID that uniquely identify graph nodes
- Example normalization:
;; Raw data
{:person/id 1 :person/name "Joe" :person/address {:address/id 42 :address/street "111 Main St."}}
;; Normalized
{:PERSON {1 {:person/id 1 :person/name "Joe" :person/address [:ADDRESS 42]}}
:ADDRESS {42 {:address/id 42 :address/street "111 Main St."}}}
- Automatic de-duplication: No out-of-sync copies
- Centralized caching: Single source of truth
- Subgraph reasoning: Load/manipulate any portion of graph
- Meteor-style subscriptions: Easy to implement with normalized data
- Co-located metadata: Query, ident, and initial state with components
- Ident function: Generates correct foreign key references
(defn person-ident [props] [:person/id (:person/id props)])
(defsc Person [this props]
{:query [:person/id :person/name]
:ident (fn [] [:person/id (:person/id props)])}
(dom/div (:person/name props)))
- Abstract operations: Like CQRS commands, serializable as data
- UI independence: UI submits data, doesn't manipulate database directly
- Multi-section structure:
action
: Local/optimistic changesremote
: Server interaction rulesok-action
/error-action
: Network result handling
(comp/transact! this `[(add-person {:name ~name})])
(defmutation add-person [params]
(action [env] ...)
(remote [env] ...)
(ok-action [env] ...))
- Co-located initial state: Optimized application startup
- Query traversal: Error checking, nested forms, UI router discovery
- Debugging: Well-defined state locations, Fulcro Inspect integration
- History traversal: Immutable snapshots for time travel
- Development snapshots: Save/restore application state
- Logic organization: Better than scattered state management
- Reusable patterns: CRUD operations, login flows
- Component roles: UI components serve roles within state machines
- Centralized debugging: State stored in normalized database