react-lean
The react-lean is a minimalist state management react library as an alternative to Redux and Mobx focused in lean coding and declarative component supporting asynchronous state, rollback handlers, loading flag, publish-subscriber, etc (only compatible with babeljs transpiler).
React state management challenge
As react state management problem is covers by three issues: verbosity, complexity and strictivity.
PROBLEMS | FEATURES |
---|
VERBOSITY | Boilerplate code and over-work coding |
COMPLEXITY | Huge detail concerns and long learning curve |
STRICTIVITY | Its high complexity demands large complex apps use cases. |
To deal with that problem and maintains the unidirectional data flow approach of React, it demands not just a new tool or library, but a new pattern.
Features
- lean state management
- async fetching support
- rollback/undo feature
- publisher-subscriber
- loading screen flag
Patterns
The flux data-flow pattern is an architectural pattern alternative to MVC and MVVM (data binding). Its main difference it its unidirectional approach with claimed better performance in comparison with MVC or MVVM. This react-lean lib follows a data flow that I named just "reflow", as bellow. The tool step is a set of library functions that changes data and data updates the view.
NAME | DIAGRAM |
---|
MVC | |
MVVM | |
FLUX | |
CCC | |
In CCC pattern, a store-component encapsulates a global object as its store, meanwhile its children state-components uses triggers a stage-commander that acts like a special "controller", but not between model and view, but between a state-component and a store-container.
A performance concern may come up with that root rendering propagating throughout multiple components, instead a single render component, but the virtual DOM react algorithm guarantees that only its differences with real DOM will be propagated. Conceptually, it is a overall rendering, but in practice, that algorithm ensure an individual case-by-case rendering component.
The stage-commander is a set of functionalities that covers three kind of component state changings: sync, async and resync. A sync state changing is the most basic state management and in React Hooks is represented by useState, meanwhile, async state changing demands useEffect hook. At last, what I call "resync state changing" is a pub-sub pattern behavior that decouples event trigger to event listener, and allowing subscribing/unsubscribing events.
In Flux/Redux there are a kind of two patterns: CQRS (reducer as handler, actions as commands, selector as queries) and pub-sub pattern (type name as 'topic', mappers as subscribing topics). It is not surprising its huge extra-work with that double pattern implementation. Although, with CCC/React-Lean, we have a kind of unidirectional MVC (as Flux), with its better performance results and, at same time, and optional pub-sub resource. Then, meanwhile flux-redux demands some scale to starting worthy due its high implementation costs and high complex data-flow, react-lean is adaptable with any application scale.
Purpose
The main purpose for a lean state management is promote a leaner and cleaner code implementing with pure functional components (or just declarative components),
- Class Component: WORSE...
class Counter extends React.Component {
constructor(props) { super(props); this.state = { count: 0 }; }
render() {
return (
<div>
<p>count: {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click
</button>
</div>
);
}
}
- Functional Component: BETTER... (with react hooks)
const Counter = props => {
const [count, setCount] = React.useState(0);
return <div>
<p>count: {count}</p>
<button onClick={() => setCount(count + 1)}>Click</button>
</div>
}
- Declarative Component: BEST... (with react-lean tools)
const Counter = props =>
<div>
<p>count: {state.count}</p>
<button onClick={() => setState(x => x.count++)}>Click</button>
</div>
Usage
The next examples is created with React Native.
Sync state management
a) storing: store{object}, state, setState(function)
Storing states is very simple, it starts with store props in Provider and is changed by a setState function, but not that this setState receives a function parameter as in follow example.
import React from 'react'
import { Provider } from 'react-lean'
import Hello from './hello'
const helloWorldStore = { name:string }
const App = props =>
<Provider store={helloWorldStore}>
<Hello />
</Provier>
import React from 'react'
import { View, Text, TextInput } from 'react-native'
import { state, setState } from 'react-lean'
const Hello = props =>
<View>
<Text>Hello {state.name || "World"}!</Text>
<TextInput value={state.name} onChangeText={texting} />
</View>
const texting = text => setState(s => s.name = text);
b) undoing: letState(function), onBack{object}
The letState allowed to rollback behavior by saving the current state a triggering the back button handler in onBack props on Provider, as exemplified in next example.
import React from 'react'
import { Provider } from 'react-lean'
import Todo from './todo'
const handler = {handler:BackHandler, trigger:'hardwareBackPress'};
const todos = { list:[], text:"", done:false }
export default App = props =>
<Provider store={todos} onBack={handler} >
<Todo title="TODO list" />
</Provider>
import React from 'react'
import { View, Text } from 'react-native'
import { Input, Icon, Checkbox } from 'react-native-elements'
import { state, setState } from 'react-lean'
const Todo = props =>
<View>
<Text>{props.title}</Text>
<Input value={state.text} onChangeText={texting} />
<Button title="Add" onPress={adding} />
<TodoList />
</View>
const TodoList = props => state.list.map((x,i) => <TodoItem key={i} {...x} />)
const TodoItem = props =>
<View style={{flexDirection:'row'}}>
<Checkbox checked={props.done} onPress={checking} />
<Text>{props.text}</Text>
<Icon name="delete" onPress={removing(props.key)} />
</View>
const adding = _ => letState(s =>s.list.push(s.item))
const texting => x => setState(s => s.text = x);
const checking => _ => setState(s => s.done = !s.done)
const removing => k => setState(s => s.list = s.list.filter((x,i) => i != k))
c) parsing: getState(string)
With getState is possible get states with string representation, for example, you could access the object state.user.name as getState("user.name") as next.
const Hello = props =>
<View>
<Text>Hello {state.name || "World"}!</Text>
<Text>Hello {getState("name") || "World"}!</Text>
</View>
Fetching: inload, onload, unload
a) async reading: inload -> unload
The inload reads a async state receiving a function as argument that, since returns a void, you can avoid imperative coding just adding a getState followed by "||" in function head as bellow.
const Todo = props => getState(listing) ||
<View>
<Text>{props.title}</Text>
<Input value={state.text} onChangeText={texting} />
<Button title="Add" onPress={adding} />
<TodoList />
</View>
The listing function examplo concludes with unload function with handle number argument. That argument manages the side effects to avoid endless rendering loop.
const listing = handle => axios
.get("http://any.api.exemple/todo")
.then(x => setState(s => s.list = x.data))
.catch(err => console.error(err))
.finally(() => unload(handle));
b) async writing: onload -> unload
The onload function wraps an event function mading three thinsgs: set loading flag to false, render the component view and prevent render looping with handle number argument.
const Todo = props => getState(listing) ||
<View>
<Text>{props.title}</Text>
<Input value={state.text} onChangeText={texting} />
<Button title="Add" onPress={adding} />
<TodoList />
</View>
const adding = e => axios
.create(someAxiosConfig)
.post("/todo", state.item)
.catch(console.error)
.finally(() => unload(e))
c) async writing-reading: onload -> inload -> unload
In a very common use case, if you want to start a loading state when requesting to insert an item with API and continue to reloading with listing function, just do that, passing on the handle argument to listing function.
const adding = e => axios
.create(someAxiosConfig)
.post("/todo", state.item)
.then(() => listing(e));
Publish-Subscribe
For complete decouple between event triggers on components and event listeners in functions, the react-lean offer an simple API for publish-subscribe pattern.
a) listening topics
A listener is a function factory that create an listener object,
const howAreYou = listener("how are you?", true, x => console.log("Great!"));
const andYou = listener("and you?", true, x => console.log("I'm fine too!"));
b) subscribing listeners
Listeners must be subscribed inside of a component, in that case, some imperative coding is needed, letting way the pure functional component for a more conventional way.
const Hello = props =>
{
subscriber(howAreYou, true);
subscriber(andYou, true);
return <View>
<Text>Hello {state.name || "World"}!</Text>
</View>
}
c) publishing topics with payloads (optional)
In next example, these buttons triggers some topics (without payloads in that example) that will catch by subscribed listeners in effortful way.
const ExampleComponent = props =>
<View>
<Button title="how are you?" onPress={_ => publisher("how are you"?)} />
<Button title="and you?" onPress={_ => publisher("and you?")} />
</View>
Reference
export class Provider {
store:any;
onLoadTimeout:number = 7000;
onBack:BackButtonEventHandler;
}
export var state:any;
export var loading:boolean;
export var subscriptions:Subscription[];
export function setState(f:changer):void;
export function letState(f:changer):void;
export function getState(f:changer):void;
export function onload(f:charger):void;
export function unload(handle:number):void;
export function inload(f:changer):void;
export function subscriber(topic:string, adding:boolean):void
export function publisher(topic:string, value:any = null):void
export function listener(topic:string, f:action, render:boolean):Listener
export interface changer { (model:any):void; }
export interface charger { (model:any):(any) => void; }
export interface action { ():void; }
class Listener { topic:string; f:action; render:boolean; }
class BackButtonEventHandler { handler:IHandler; trigger:string; }
class Subscription { topic:string; render:boolean; action:()=>void; }
export interface IHandler { addEventListener(handler:any, trigger:string) }