f.lux Overview

What is f.lux

f.lux is a data management library inspired by the React flux architecture and influenced by Redux and immutable.js. This is third version of the f.lux approach and the first to be published open source. This version is actively used in commercial projects on both web and react-native platforms and really shines when implementing complex data-rich applications, such as enterprise, form-heavy, and complex business logic applications.

High-level feature list

Single store

All application state is stored in a single object tree as inspired by Redux’s single store approach. A single source of truth greatly simplifies application state management, reduces complexity, and eases debugging.

Co-locate state and action functions

The f.lux store efficiently virtualizes the state tree into a shadow state as inspired by the React shadow DOM. The virtualization process, called shadowing, binds action-type functions onto the state tree properties. Binding the functions with the data makes explicit the operations that may be performed on a state tree property. The virtualization process can also expose virtualized properties for the underlying data that when set using = generates an update action without having to write any code. The exposed properties may also be set to readonly and will ensure the value is not updated, handy for object IDs.

The virtualization process is central to the f.lux architecture and is designed to be simple for common cases while exposing a straight-forward, declarative mechanism to customize the process on a per property basis. The application works with shadow objects that are accessed just like traditional javascript objects. This means your application logic looks “normal” and you can interact and inspect state objects in the javascript console like regular objects. The shadow objects are immutable so interacting with them through property assignments and function invocation result in actions being dispatched to the store for in order processing. The store then generates a change event on the next tick for the application to process the new state.

To be explicit, the virtualization process is recursively applied to the entire state tree. For performance reasons, the process occurs on a just-in-time basis so only the accessed properties are virtualized.

Immutable state

All state changes occur indirectly through actions, usually through shadow object bound functions and properties.

Making changes through actions ensures changes happen in a strict order and store listeners receive a coherent and atomically updated state tree.

Property life-cycle

State tree properties have a life-cycle that analogous to the React component life-cycle.

The property life-cycle is:

The life-cycle motivating use case was integrating with state changes external to the application code. External state changes examples include:

The virtualization architecture divides a shadow property into two entities:

Here is an example of a react-native orientation property:

import Orientation from "react-native-orientation";
import {
	ObjectProperty,
	PrimitiveProperty,
	Shadow,
	StateType,
} from "f.lux";
import { shadow } from "f.lux/lib/decorators";

import appDebug, { AppOrientationPropertyKey as DebugKey } from "./debug";
const debug = appDebug(DebugKey);


export const LandscapeOrientation = "LANDSCAPE";
export const PortraitOrientation = "PORTRAIT";
export const UnknownOrientation = "UNKNOWN";


export default class OrientationProperty extends ObjectProperty {
	propertyWillShadow() {
		// regiseter for orientation events (not showing removing orientation lister to shorten example)
		Orientation.addOrientationListener(orientation => this._onOrientationChange(orientation) );

		// get the current orientation (async op)
		Orientation.getOrientation( (err, orientation) => {
				if (err) {
					debug(`getOrientation() error`, err);
				} else {
					this._onOrientationChange(orientation);
				}
			});
	}

	_onOrientationChange(orientation) {
		if (orientation === UnknownOrientation) {
			return debug(`_onOrientationChange(): ignoring orientation update - orientation=${orientation}`);
		}

		// update the 'direction' property with the new orientation
		this._keyed.set("direction", orientation);

		debug(`_onOrientationChange() orientation update: ${orientation}`);
	}
}

The statement this._keyed.set("direction", orientation) demonstrates using the ObjectProperty class’ method to set/update read-only virtualized properties (code not shown declaring direction as readonly). Most of the time, however, you will interact with the shadow model using standard javascript techniques, assignment and functions/methods. The UI could access the direction property using

import { LandscapeOrientation } from "./OrientationProperty";

...

const { orientation } = store.shadow;

console.log(orientation.direction);

if (orientation.direction === LandscapeOrientation) {
	// do something specific to landscape mode here
} else {
	// do something portriat specific here
}

The direction property would be an ideal candidate for being readonly so the application logic could not change it. The OrientationProperty could still change it using the line this._keyed.set("direction", orientation);. This is easy to accomplish though the mechanism is not shown here and is explored in the tutorial.

Implementing properties is one of the few cases where f.lux utilizes inheritance, which feels natural since this is defining a new type. The f.lux approach is to avoid defining Property class hierarchies. A facility is provided for sharing functionality through mixins when the shared code needs to tie into the property life-cycle; a rarely needed but useful capability to have when desired.

Collections (remote data)

f.lux provides built-in support for working with remote data through the Collection property type.

The Collections api is inspired by the simple (Backbone)[] collections for performing CRUD operations on remote data. The data operations are implemented through an abstraction called an endpoint. F.lux ships with support for REST and local endpoints. The local endpoint is often used for stub data during development and testing. Custom endpoints have been written for Couchbase and GraphQL data sources.

Collections live in the virtualized state tree like any other property and store all information in the actual state tree making them time travel compatible.

Logging and time travel debugger

F.lux includes a logging facility and time travel debugger accessed through the javascript console.

Both facilities are exposed through a single console object. The help command lists the following:

f.lux logger commands:
	back          - moves backward in time by one store state frame
	clear         - removes all logs
	forward       - moves forward in time by one store state frame
	help          - f.lux logger commands
	index         - active index of store state frames
	maxFrames     - # of store updates to cache (default=50)
	print         - print logs to console
	printNoState  - print logs to console without state objects
	size          - # of store state frames available
	store         - gets the f.lux store

Functions:
	clearTrap(name)                    - clears a trap set by 'setTrap()'
	goto(idx)                          - move to a specific store state frame
	setMaxFrames(maxFrames)            - set the maximum number of store states to maintain (default=50)
	setTrap(cond, value, name=uuid)    - sets a debugger trap and returns name. Condition argument may be
	                                     a function taking next state or a string path to get a value
	tail(count=10, printState=true)    - prints last 'count' store updates


f.lux log available at window.flog

The store api used for implementing the logger and time travel debugger can be utilized to implement alternative implementations.

react-ui

An add-on module providing React support for mapping store state to React component properties, collection mappings, and form components.