Don't Fear the Fractal: Infinite State Composition with Freactal

May 3, 2017
Tyler ThompsonTyler Thompson

In the beginning, there was React. You used createClass to define an OOP-style UI component with associated reactive state, and custom mixins managed cross-cutting concerns. It was good.

Then came ES6 classes. You used standard language features and semantics to define a component instead of using a custom userspace one. With no mixin feature in ES6 classes, mixins fell to the wayside. It was good.

Then came stateless functional components. You defined a component as a pure function of props, and for a large number of components, you could explicitly ignore state. It was good.

Then came the rediscovery of "functional setState." Now, you could treat state changes in your stateful components as pure functions of props and state. It, too, was good.

Notice the progression? New React releases and patterns chip away at OOP features and add functional ones. Mixins, an unambiguously object-oriented feature, died with ES6 classes. For stateless components, pure functions replaced classes. For "container" components, setState functions deemphasized the stateful features of classes.

React didn't move in this direction in a vacuum: the JS ecosystem, now embracing functional programming, demanded and informed these changes. Even now, the shift from OOP to functional programming continues to drive the feedback loop between React and the JS community.

Of course, the conversation isn't always in lockstep. Constricted by the limitations (perceived or otherwise) of setState and yearning for powerful dev tooling, open sorcerers unleashed a Cambrian explosion of state management libraries, the most popular being Redux and MobX. While agnostic to choice of view library, both Redux and MobX provide React bindings and show up in the typical React "stack".

Current state containers don't have a concept of "components." Redux prescribes a global state tree that isn't in relation to any specific element of a UI. MobX provides primitives that don't care about where they live.

Are React apps as powerful when state and components live in different trees? As much as we love our new FP overlords, did we lose something valuable in our sprint away from OOP? Do new approaches to state violate the core contract of React?

Interface vs. Implementation

What is the core contract of React? It's certainly not SFCs, HOCs, extends Component, or createClass. It's not lifecycle methods, and it's not elements, instances, classes, or JSX.

The fundamental contract of React is the component. The only axioms of a component are:

  • A component manages and contains its own state.
  • A component is composed of, or composes, other components in an infinitely extendable tree.
  • The concrete realization of a component is a pure derivation of props and state.

What do these axioms enable?

  • Model and view are colocated. Since they are both of a given component's concern, they demand high locality (i.e. they're not five directories away).
  • The APIs for parents and children are identical. This minimizes API surface area and removes the need for special "orchestrators."
  • Rendering is predictable and unidirectional. Nothing from the depths of the component tree will mutate the result. It's props and state all the way down.

Components are the real promise of React. Reactive state isn't new. Neither is colocation of model and view. It's the combination of both, married to an infinitely recursive and composable API, that allows a React component to "shrink the universe" and consider a single application concern at a time.

Breach of Contract

How well do new approaches to state fit into the component model?

Let's pick on Redux for a minute, only because it's one of the more prescriptive state containers. Redux breaks two axioms of the component contract:

  • A component manages and contains its own state.
    • While you can inject slices of the state tree into a component using react-redux, that state is still a property of the state tree, not the component. You lose the scannable, explicit dependency of a hierarchical parent-child relationship, not to mention that any component a mile away can create an implicit dependency on a global state slice.
  • A component is composed of, or composes, other components in an infinitely extendable tree.
    • The Redux API is not self-similar. This means that the top-level "orchestrator" API (createStore, middleware, enhancers, etc) does not resemble the API of its children (components). Therefore, stores cannot compose each other. Self-similarity is required for infinitely recursive, hierarchical, "fractal" architecture.

The Hunt for Red OOPtober

The dawn of Redux coincided with the community's thirst for purely functional approaches to state. While quenching this thirst, we chugged a little too fast. We lost a key property of the component model, assuming it to be an OOP relic: encapsulation.

Just as the promise of React isn't its implementation, encapsulation isn't access control modifiers, class properties, or grouped bags of mutable state and methods. Encapsulation is containing and restricting access to data. Without encapsulation, components have no boundaries. Their universe becomes as infinite as our own.

Functional programming has encapsulation, and its implementation contains superpowers not available to OOP: composition. Not only does composition guarantee a defined boundary of data (e.g. arguments of a function or bindings in scope), it also unlocks self-similarity and hierarchy. React components couldn't be fractal if each component defined its own bespoke API, hence the self-similar signature of props and state.

Given that functional encapsulation exists, why don't we find it in Redux? We can compose reducers, but this composition only encapsulates the transformation of the tree, not the tree itself. We can't compose stores, since the "orchestrator" API isn't fractal. We can't compose middleware, since middleware is order-dependent. We can compose enhancers, but only if they all behave.

Can we marry a functional approach to state, derivations, and effects to the component model of React? Can we obviate a slew of extensions (reselect, redux-loop, redux-thunk, redux-saga) in the process?

Our engineer Dale Bustad did just that, and even brought fun back to state management, with Freactal.

Introducing Fr(e)actal

Freactal is a functional one-stop-shop for state, derivation, and effect management in React. It acknowledges the power of the Redux ecosystem while remaining true to the promise of React.

In Freactal, a state container is just a component. You're free to compose at any level of the component hierarchy with any granularity of state.

Freactal handles state encapsulation, state injection into component children, effects, and computed values.

Encapsulating state with provideState

Components are the only state container in Freactal, replacing the global state atom of Redux. The provideState higher-order component defines a "schema" for your component's state, as well as its initial state on mount.

Here's an example of provideState creating a stateful component:

import React, { Component } from "react"; import { render } from "react-dom"; import { provideState } from "freactal"; // Creates a higher-order component that provides a state // schema and initial state to a wrapped component const wrapComponentWithState = provideState({ initialState: () => ({ counter: 0 }) }); const Parent = wrapComponentWithState(({ state }) => ( <div> { `Our counter is at: ${state.counter}` } </div> )); render(<Parent />, document.getElementById("root"));

Note that provideState only sets up the state schema and initial state–it does not provide access to any subsequent state changes. This allows provideState to define reusable "state templates" to apply to multiple independent components.

To access the live state values, wrap a component with injectState. injectState adds superpowers to child components of provideState-wrapped components: the wrapped component will only re-render for pieces of state the component uses, rather than for each state change.

If we update our previous example with injectState:

import React, { Component } from "react"; import { render } from "react-dom"; import { provideState, injectState } from "freactal"; const wrapComponentWithState = provideState({ initialState: () => ({ counter: 0, name: "Bob" }) }); const Parent = wrapComponentWithState(injectState(({ state }) => ( <div> { `Our counter is at: ${state.counter}` } </div> ))); render(<Parent />, document.getElementById("root"));

...that means that Parent will only re-render when state.counter changes.

provideState shines when used in a component hierarchy. Child components using injectState can access state provided by parents, grandparents, and so on, all the way up the tree. The only rule: if two ancestors contain conflicting state keys, the nearest ancestor's key/value takes precedence.

Watch as a child component accesses state from a parent and a grandparent:

const Child = injectState(({ state }) => ( <div> This is the GrandChild. {state.fromParent} {state.fromGrandParent} </div> )); const Parent = provideState({ initialState: () => ({ fromParent: "ParentValue" }) })(() => ( <div> This is the Child. <GrandChild /> </div> )); const GrandParent = provideState({ initialState: () => ({ fromGrandParent: "GrandParentValue" }) })(() => ( <div> This is the Parent. <Child /> </div> ));

Effective Immediately

Freactal combines side effects and state updates into a single abstraction, simply named "effects." Effects resemble an action/reducer pair in Redux, but they embrace async as a first principle and allow for composable handling of intermediate state.

provideState accepts an effects key with a value of named effect definitions. An effect is just a function with the signature:

(effects, ...args) => Promise<state => newState>

The effects argument contains all of the defined effects in this container, as well as the effects defined by its ancestors. ...args are any arguments you pass when calling the effect (e.g. the 10 in effects.fetchPosts(10)). That function returns a Promise that resolves to an updater function, state => newState, which resembles a Redux reducer.

Don't memorize that, though! It's painless in practice, and partial application is the bedrock for the powers of effects.

Simple state changes are concise, since freactal automatically Promise.resolves any synchronous updater functions provided:

{ addOne: () => state => Object.assign({}, state, { counter: state.counter + 1 }) }

Even easier, you can use the softUpdate helper to replace the common Object.assign pattern:

// { counter: 1 } { increment: softUpdate((state, increment) => ({ counter: state.counter + increment })) } effects.increment(5) // { counter: 6 }

This makes the bread and butter of state management a breeze.

Async effects don't require special treatment, since promises already provide the plumbing. If you define this effect:

provideState({ initialState: () => ({ posts: [] }), effects: { fetchPosts: (effects, limit) => fetch(`/api/posts?limit=${limit}`) .then(result => result.json()) .then(posts => state => Object.assign({}, state, { posts })) } });

...you can call it like this in a child component:

const Child = injectState(({ state, effects }) => { const onClick = () => effects.fetchPosts(10); return ( <div> { state.posts.map(post => <p>{post.body}</p> ) } <button onClick={onClick}>Fetch some posts!</button> </div> ); });

...and your state becomes:

{ posts: [ { title: "Freactal!", body: "Freactalreactalactalctaltalall" }, ... ] }

The real fun comes from composing effects together. Since effects just return promises, you compose them in a regular promise chain! You get the benefits of a linear sequence of side effects and intermediate state changes without coupling effects to a specific sequence, like in redux-thunk or redux-saga. Furthermore, subsequent effects live in the same expression. You can follow an entire trail of consequences within a "sentence" of code, unlike redux-loop or Elm where the sequence of effects is obscured by vertical "loops" around the reducer. Finally, the partial application of effects allows freactal to assemble and schedule state changes correctly while providing an indication of "completion" (a resolved promise), especially useful for server-side rendering and data fetching.

Effect composition is great for indicating transient loading states, something notoriously fickle and error-prone in other solutions like redux-thunk. Here's an extended post-fetching example that sets a pending boolean state throughout a request cycle:

const wrapComponentWithState = provideState({ initialState: () => ({ posts: null, postsPending: false }), effects: { setPostsPending: softUpdate((state, postsPending) => ({ postsPending })), getPosts: effects => effects.setPostsPending(true) .then(() => fetch("/api/posts")) .then(result => result.json()) .then(({ posts }) => effects.setPostsPending(false).then(() => posts)) .then(posts => state => Object.assign({}, state, { posts })) } });

Does Not Compute

Freactal obviates the need for the existing ecosystem of async/effects solutions popular with Redux. So what else can it consolidate?

A common goal in immutable unidirectional architectures is to derive all data from the minimum amount of state possible without unnecessary recalculation. The solution usually manifests itself as "computed" values: memoized derivations that automatically recalculate (or don't) on state changes. Redux users typically turn to reselect for this exact purpose. MobX treats computed values as its core primitive.

Freactal provides an API for computed values, with the added benefit of colocating them next to the state they derive from. Just as in reselect and MobX, any derivation can depend on a previous derivation and work transparently.

Computed properties are just functions that are provided state and that return a derivation:

const wrapComponentWithState = provideState({ initialState: () => ({ givenName: "Walter", familyName: "Harriman", locale: "en-us" }), effects: { setGivenName: softUpdate((state, val) => ({ givenName: val })), setFamilyName: softUpdate((state, val) => ({ familyName: val })) }, computed: { fullName: ({ givenName, familyName, locale }) => startsWith(locale, "en") ? `${givenName} ${familyName}` : `${familyName} ${givenName}`, greeting: ({ fullName, locale }) => startsWith(locale, "en") ? `Hi, ${fullName}, and welcome!` : `Helló ${fullName}, és szívesen!` } });

fullName and greeting will now both be accessible to child components as if they were properties on state.

Like other derivation libraries, Freactal's computed values are memoized and invalidated when its dependent state changes. Freactal adds laziness to the picture: if a computed value greeting is never accessed, it is never computed!

To Infinity and Beyond

Freactal's fractal architecture, combined with its consolidated toolbelt for managing state, renews the component contract and embraces the true promise of React.

Freactal is still under development, and we'd appreciate any feedback! Feel free to open a PR or ping us on Twitter with questions and ideas. A fancy set of devtools and logging middleware are on their way, alongside documentation additions.

This is just a glimpse of Freactal. Dale's written a wonderful guide where he deep-dives on the power of effects and explains other concepts like middleware and state update helpers. Oh, and his logo's pretty slick too.

Go check out Freactal on GitHub!

Related Posts

Ranked Choice Voting: The Mobile Challenge

November 19, 2024
While working on VoteHub, a mobile absentee ballot solution for U.S. elections, I was tasked with designing and prototyping an interface for a relatively new election contest type, rapidly gaining attention and adoption, called Ranked Choice Voting (RCV).

Empowering Users: Developing Accessible Mobile Apps using React Native

July 2, 2024
Robust technical accessibility strategies and best practices we implemented for a mobile voting application using React Native.

Seamless Transitions: From Native to React Native

June 4, 2024
React Native, developed by Meta, allows developers to use a single codebase to create apps that run on both iOS and Android