Fractal
The most popular state management model in today’s front-end domain is undoubtedly Redux, but unfortunately redux is not a fractal architecture. What is fractal architecture?
If the sub-components can be used as an application in the same structure, such a structure is a fractal architecture.
In fractal architecture, each application is organized into a larger application, while in non-fractal architecture, applications tend to rely on a global orchestrator, and all components cannot be used as applications in the same structure, but harmonize with this orchestrator. For example, redux focuses only on state management and does not involve the view implementation of components, which does not constitute a complete application closed loop. Therefore, Redux is not a fractal architecture. In Redux, the coordinator is the global Store.
Let’s look at the source of Redux’s inspiration — Elm:
Under the Elm architecture, each component has a complete application loop:
- A Model type
- An initial instance of Model
- A View function
- An Action Type and the corresponding update function
Therefore, Elm is a fractal architecture, and each Elm component is an Elm application.
Cycle.js
The advantages of fractal architecture are obvious, that is, it is easy to reuse and combine. Cycle.js also advocates fractal architecture. It abstracts the application into a pure function main(sources). This function receives a sources parameter and is used to get side effects such as DOM and HTTP from the external environment, and then outputs corresponding sinks to influence the external environment.
Based on this simple and direct abstraction, cycle.js is easy to achieve fractal, i.e. each cycle.js application (each main function) can be combined into a larger cycle.js application:
run
import {run} from '@cycle/run'
import {div, label, input, hr, h1, makeDOMDriver} from '@cycle/dom'
function main(sources) {
const input$ = sources.DOM.select('.field').events('input')
const name$ = input$.map(ev= > ev.target.value).startWith(' ')
const vdom$ = name$.map(name= >
div([
label('Name:'),
input('.field', {attrs: {type: 'text'}}),
hr(),
h1('Hello ' + name),
])
)
return { DOM: vdom$ }
}
run(main, { DOM: makeDOMDriver('#app-container')})Copy the code
State management of cycle.js
responsive
As mentioned above, Cycle.js promotes fractal application structure. Therefore, state manager like Redux is not what Cycle.js is willing to use. Based on this, if state management model is to be introduced, its design should not change the basic structure of cycle. js application: receiving sources from the external world and exporting sedimentations to the external world.
In addition, since Cycy.js is a responsive front-end framework, state management remains responsive, based on stream/ Observable. If you’re familiar with responsive programming, it’s easy to implement a state management model based on the Elm concept, using RxJs as an example:
const action$ = new Subject()
const incrReducer$ = action$.pipe(
filter(({type}) = > type === 'INCR'),
mapTo(function incrReducer(state) {
return state + 1}))const decrReducer$ = action$.pipe(
filter(({type}) = > type === 'DECR'),
mapTo(function decrReducer(state) {
return state - 1}))const reducer$ = merge(incrReducer$, decrReducer$)
const state$ = reducer$.pipe(
scan((state, reducer) = > reducer(state), initState),
startWith(initState),
shareReplay(1))Copy the code
Based on the above premise, the basic design of cycle. sJ state management model also appears on the paper:
- To state the source
state$
In thesources
To the cycle.js application - The cycle.js application willreducer$In the
sinks
Output to the outside world
See the @cycle/ State source code for withState, which implements the reactive state management model in much the same way.
In practice, cyces.js infuses the state management model for cyces.js with the withState exposed by @cycle/state:
import {withState} from '@cycle/state'
function main(sources) {
const state$ = sources.state.stream
const vdom$ = state$.map(state= > /*render virtual DOM*/)
const reducer$ = xs.periodic(1000)
.mapTo(function reducer(prevState) {
// return new state
})
const sinks = {
DOM: vdom$,
state: reducer$
}
return sinks
}
const wrappedMain = withState(main)
run(wrappedMain, drivers)
Copy the code
After considering how to keep the fractal of cycle.js after introducing the state management model, we need to solve the following problems in the state management model:
- How do I declare the initial application state
- How does an application read and modify a state
Initialization state
To follow responsiveness, we can declare an initReducer$that by default emits an initReducer, which directly returns the initial state of the component:
const initReducer$ = xs.of(function initReducer(prevState) {
return { count:0}})const reducer$ = xs.merge(initReducer$, someOtherReducer$);
const sinks = {
state: reducer$,
};
Copy the code
Use the Onion model to pass state
In real projects, applications are often composed of multiple components, and there is a hierarchy between components, so you need to think about:
- How do I pass state to a component
- How to pass Reducer to external
Suppose our state tree is:
const state = {
visitors: {
count: 300}}Copy the code
Assuming that our component requires count state, there are two design approaches:
(1) Declare the state to be extracted directly in the component, how to deal with the change of sub-state:
function main(sources) {
const count$ = sources.state.visitors.count
const reducer$ = incrAction$.mapTo(function incr(prevState) {
return prevState + 1
})
return {
state: {
visitors: {
count: reducer$
}
}
}
}
Copy the code
(2) Keep the component pure, the state$obtained and the reducer$output do not consider the current state tree form, both of which are only relative to the component itself:
function main(sources) {
const state$ = sources.state
const reducer$ = incrAction$.mapTo(function incr(prevState) {
return prevState + 1
})
return {
state: reducer$
}
}
Copy the code
Both approaches have their advantages. The first approach is more flexible and is suitable for scenarios with deep hierarchy nesting. The second makes component logic more cohesive, with greater component autonomy, and may be more straightforward in simple scenarios. Here we begin with a second way of transferring states.
In the second state passing mode, we want to pass count to the corresponding component, we need to peel the state layer by layer from the outside to the inside, until we get the required state of the component:
stateA$ // Emits object `{visitors: {count: 300}}}`
stateB$ // Emits object `{count: 300}`
stateC$ // Emits object `300`
Copy the code
When the components output reducer, reduce needs to be done from inside to outside:
reducerC$ // Emits function `count => count + 1`
reducerB$ // Emits function `visitors => ({count: reducerC(visitors.count)})`
reducerA$ // Emits function `appState => ({visitors: reducerB(appState.visitors)})`
Copy the code
This leads to an onion-like state management model: We start with the external world, peel back layers and get states; Perform reduce operations layer by layer and update status from inside to outside:
As an example, suppose the parent component gets the following state:
{
foo: string,
bar: number,
child: {
count: number,
},
}
Copy the code
Where, child state is the state required by its child component. In this case, the onion model should consider:
- will
child
Stripped from the state tree and passed to child components - Collect the output of the child component
reducer$
, continue to output after merging
First, we need to isolate the child components using @cycle/ ISOLATE, which exposes a ISOLATE (Component, scope) function that takes two parameters:
component
: The component that needs to be isolated, that is, an acceptancesources
And returnsinks
The function ofscope
: Scope to which the component is isolated. Scope determines how external environments such as DOM, state, etc. divide their resources into components
This function will eventually return sinks of the isolation component output. After obtaining the reducer$of the child component, merge it with the Reducer $of the parent component and continue to pour out.
For example, in the following code, the ISOLATE (Child, ‘Child ‘)(Sources) isolates the Child component under a scope named Child, so @cycle/state knows, To select a state subtree named Child from the state tree for the Child component.
function Parent(sources) {
const state$ = sources.state.stream; // emits { foo, bar, child }
const childSinks = isolate(Child, 'child')(sources);
const parentReducer$ = xs.merge(initReducer$, someOtherReducer$);
const childReducer$ = childSinks.state;
const reducer$ = xs.merge(parentReducer$, childReducer$);
return {
state: reducer$
}
}
Copy the code
PrevState === undefined; prevState == undefined; prevState == undefined; prevState == undefined; prevState == undefined; prevState == undefined; prevState == undefined; prevState == undefined; prevState == undefined
function Child(sources) {
const state$ = sources.state.stream; // emits { count }
const defaultReducer$ = xs.of(function defaultReducer(prevState) {
if (typeof prevState === 'undefined') {
return { count: 0}}else {
return prevState
}
})
// Here, Reducer will handle {count} state
const reducer$ = xs.merge(defaultReducer$, someOtherReducer$);
return {
state: reducer$
}
}
Copy the code
As a good practice, we declare a defaultReducer$for each component, which takes care of the scenario when it is used alone, as well as when there is a parent component.
See the cycle.js Components section for a reason for component isolation
The Lens mechanism is used to transfer status
In the Onion model, data is passed from the parent to the child, where the parent can only pick a child tree from its own state tree, so the model is limited in flexibility:
- Numerically: Only one substate can be passed
- On scale: Cannot pass the entire state
- On I/O: The status can be read but cannot be modified
This model will not work if you have the following requirements:
- Components require multiple states, such as need to obtain
state.foo
及state.status
- Parent and child components need to access the same part of state, such as parent and child components need to obtain
state.foo
- When the state of a child component changes, the state tree needs to be modified by linkage instead of just passing
reducer$
Modify its own state
To do this, consider using the first state-sharing approach we mentioned above. While we’re somewhat sketchy, Cycle.js introduces lens to handle scenarios that the Onion model can’t handle, which, as the name suggests, gives the component the ability to see (read) and change (write) state.
In simple terms, Lens defines reads and writes to data through getter/setter.
In order to read and write state through Lens, cycli. js makes the ISOLATE accept lens as a scope selector for component customization when isolating component instances to show how @cycle/ State components read and modify state.
const fooLens = {
get: state= > state.foo,
set: (state, childState) = >({... state,foo: childState})
};
const fooSinks = isolate(Foo, {state: fooLens})(sources);
Copy the code
In the code above, by customizing lens, component Foo can obtain the state of Foo in the state tree, and when Foo modifies Foo, the state of Foo in the state tree will be modified.
Working with dynamic lists
Rendering dynamic lists is one of the most common requirements of the front end, and before cycle.js introduced state management, this was one of the things cycle.js failed to do, and Andre Staltz even wrote an issue on how to handle dynamic lists more gracefully in Cycle.js.
Now, based on the state management model above, a makeCollection API is needed to create a dynamic list in cycle.js:
function Parent(sources) {
const array$ = sources.state.stream;
const List = makeCollection({
item: Child,
itemKey: (childState, index) = > String(index),
itemScope: key= > key,
collectSinks: instances= > {
return {
state: instances.pickMerge('state'),
DOM: instances.pickCombine('DOM')
.map(itemVNodes= > ul(itemVNodes))
// ...}}});const listSinks = List(sources);
const reducer$ = xs.merge(listSinks.state, parentReducer$);
return {
state: reducer$
}
}
Copy the code
To create a dynamic list based on @cylce/state, we need to tell @cycle/state:
-
What are the list elements
-
The position of each element in the state
-
Scope for each element
-
Reducer $: instances. PickMerge (‘state’), which equals
xs.merge(instances.map(sink => sink.state))
-
List vDOM $: instances. PickCombine (‘DOM’), which equals approximately:
xs.combine(instances.map(sink => sink.DOM))
To add a list element, add an element to the array in reducer$of the list container:
const reducer$ = xs.periodic(1000).map(i= > function reducer(prevArray) {
return prevArray.concat({count: i})
})
Copy the code
Deletion of elements requires that the status of the child component be marked as undefiend when the deletion behavior is triggered. Accordingly, cycle. js would delete the status from the list array, and then delete the child component and its output collocation:
function Child(sources) {
const deleteReducer$ = deleteAction$.mapTo(function deleteReducer(prevState) {
return undefined;
})
const reducer$ = xs.merge(deleteReducer$, someOtherReducer$)
return {
state: reducer$
}
}
Copy the code
conclusion
Cycle.js is a niche framework compared to the top three front-end frameworks (Angular/React/Vue). Learning such a framework is not for the sake of novelty, and it is hard to use it as a support framework for large projects, given your team. However, this does not prevent us from getting inspiration and inspiration from cycle. js design, which can make you feel more or less:
- Maybe our app is a ring that deals with the outside world
- What is a fractal
- The magic of responsive programming
- What is lens? How do I use Lens in JavaScript applications
- .
In addition, Cycle.js author Andre Staltz is also a charming and expressive developer, and I recommend you to follow him:
- Andre Staltz blogs at staltz.com/
- A multitude of framework design ideas and apis from Andre Staltz’s RxJs and Cyces.js tutorial on Egghead. IO
- Andre Staltz attends and speaks at a series of conferences: www.youtube.com/results?sea…
Finally, don’t idolize, just learn and explore like crazy.
The resources
-
UNIDIRECTIONAL USER INTERFACE ARCHITECTURES
-
Cycle State
-
An Introduction Into Lenses In JavaScript