https://github.com/ecomfe/react-kiss
A simple and stupid react container solution
https://github.com/ecomfe/react-kiss
Last synced: 11 months ago
JSON representation
A simple and stupid react container solution
- Host: GitHub
- URL: https://github.com/ecomfe/react-kiss
- Owner: ecomfe
- License: mit
- Created: 2018-08-02T05:01:57.000Z (almost 8 years ago)
- Default Branch: master
- Last Pushed: 2022-12-08T09:08:01.000Z (over 3 years ago)
- Last Synced: 2025-06-30T01:05:30.744Z (about 1 year ago)
- Language: JavaScript
- Size: 1.37 MB
- Stars: 84
- Watchers: 12
- Forks: 5
- Open Issues: 25
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# react-kiss
[Redux](https://redux.js.org/) and [mobx-state-tree](https://github.com/mobxjs/mobx-state-tree) are both good state container solutions, however they introduces too many terminologies and bioperlates to structure a simple app.
We want something super simple and stupid, something that can define a state container everywhere at any granularity and combine them together to form a complete app. react-kiss is thus a state container which:
- introduces minimal efforts to create and use states.
- ships with both state and state transfer definitions, handles sync and async workflows naturally.
- allows split states to different parts as small as possible and combine then together when required.
- encourages establishing and joining state container in a smaller scope instead of a monotonous global state.
## Install
```shell
npm install react-kiss
```
## Region
A region is a container of a state and some workflows to manipulate state in the context of a given payload.
### State
A state is a predefined structure and its current data, any plain object can be a state.
### Workflow
A workflow is a process which receives a payload and manipulates current state, a workflow can manipulates state either synchronously or asychronously, it is also possible manipulates state multiple times within a workflow.
There are 2 forms of workflows.
#### Simple workflow
A simple workflow is a simple function that receives a payload and current state, it should return either a state patch like:
```javascript
const setCurrentUser = (user, state) => {
if (state.currentUser) {
return {};
}
return {
currentUser: user
};
};
```
or a state updater function like:
```javascript
const addValue = (amount, state) => {
if (state.value >= 100) {
return {};
}
return ({value}) => ({value: value + amount});
};
```
#### Composite workflow
A composite workflow is a workflow which may manipulates state multiple times or involves async process, it is defined as a generator function:
```javascript
function* saveTodo(todo) {
yield {submitting: true};
try {
const newTodo = yield postTodo(todo);
yield state => {
const {todos} = state;
return {
todos: [...todos, newTodo],
submitting: false
};
};
}
catch (ex) {
yield {submitting: false, error: ex};
}
};
```
This generator function receives `(payload, getState)` as its arguments, and yields value in 3 types:
- a simple object is treated as a state patch.
- a function is treated as a state updater.
- a `Promise` instance is treated as an async process, its resolved value or rejected error will returned back to `yield` expression.
### Selector
A selector is a pure function which computes and selects certain values from current state, selectors are defined as an object with function values:
```javascript
const selectors = {
filterVisibleTodos({todos, filter}) {
return filter ? todos.filter(todo => todo.includes(filter)) : todos;
}
};
```
Selectors can also receive arbitary arguments, the first argument is always the `currentState`, rest arguments are those passed to selector on invocation.
When invoke a selector, the `currentState` argument is omitted (it is bound automatically), so the above selector is called just as `const todos = filterVisibleTodos()`;
## Define a region
To define a region, we just need to provide an `initialState` and a map of `workflows` to `defineRegion` exported function:
```javascript
import {defineRegion} from 'react-kiss';
const initialState = {
todos: [
'Buy milk',
'Meet John at peace park'
],
filter: '',
error: null,
submitting: false
};
const workflows = {
* saveTodo(todo) {
yield {submitting: true};
try {
const newTodo = yield postTodo(todo);
yield state => {
const {todos} = state;
return {
todos: [...todos, newTodo],
submitting: false
};
};
}
catch (ex) {
yield {submitting: false, error: ex};
}
},
filterByKeyword(keyword) {
return {filter: keyword};
}
};
const selectors = {
filterVisibleTodos({todos, filter}) {
return filter ? todos.filter(todo => todo.includes(filter)) : todos;
}
};
const todoRegion = defineRegion(initialState, workflows, selectors);
export const establishTodo = todoRegion.establish;
export const joinTodo = todoRegion.join;
```
The return value of `defineRegion` function is an object containing `establish` and `join` function.
## Establish a region
By `defineRegion` we get a region definition but it is not yet usable as a state container, we should establish it at a parent scope and join it from it's children.
To establish a region, call `establish` function returned by `defineRegion` like an HOC:
```jsx
import {establishTodo} from 'regions';
const Todo = () => (
);
export default establishTodo('Todo')(Todo);
```
The only argument of `establish` function is an optional name of region, by enhancing a component with `establish`, it now acts as a context's `Provider` to manage the state.
Note a region can be established in different places, just like using `Provider` in different places, a child receives state from the closest regions of same type.
## Join a region
All children components under a component enhanced with `establish` can choose to join this region by invoking `join` function returned from `defineRegion`, in case a component is joined to a region, it automatically receives state and workflows from region, a `mapToProps` function is used to select props:
```jsx
import {PureComponent} from 'react';
import {bind} from 'lodash-decorators';
import {joinTodo} from 'regions';
class AddTodo extends PureComponent {
state = {
todoText: ''
};
@bind()
syncTodoText(e) {
this.setState({todoText: e.target.value});
}
@bind()
async saveTodo() {
const {todoText} = this.state;
const {onSaveTodo} = this.props;
await onSaveTodo(todoText);
this.setState({todoText: ''});
}
componentDidUpdate(prevProps) {
if (this.props.error !== prevProps.error) {
alert(this.props.error.message); // eslint-disable-line no-alert
}
}
render() {
const {todoText} = this.state;
const {submitting} = this.props;
return (
{submitting ? 'Submitting...' : 'Add Todo'}
);
}
}
const mapToProps = ({submitting, error, saveTodo}) => ({submitting, error, onSaveTodo: saveTodo});
export default joinTodo(mapToProps)(AddTodo);
```
This is very similar to react-redux's `connect` function except it only requires one `mapToProps` function.
## Combine regions
We can establish region at any place, it is also straightforward to establish multiple regions with different types:
```javascript
import {compose} from 'recompose';
import {establishTodo, establishNote} from 'regions';
const App = () => (
// ...
);
const enhance = compose(
establishTodo('MyTodo'),
establishNote('Note')
);
export default ehnahce(App);
```
Note that it is **NOT** OK to establish multiple regions with the same type (returned from the same `defineRegion` call), in such case only the latest region takes effects.
We can also join multiple regions using the `joinAll` exported function:
```jsx
import {compose} from 'recompose';
import {joinAll} from 'react-kiss';
import {establishNote, joinNote, joinGlobal} from 'regions';
const Note = ({username, visible, message, onToggle}) => (
{visible ? 'Hide' : 'Show'}
{visible && {message} @ {username}
}
);
const mapToProps = (note, global) => {
const message = note.notes[global.username];
return {
username: global.username,
message: message,
visible: note.visible,
onToggle: note.toggle
};
};
const enhance = compose(
establishNote('Note'),
joinAll(joinNote, joinGlobal, mapToProps)
);
export default enhance(Note);
```
The `joinAll` function receives multiple `join` functions and a `mapToProps` function, the `mapToProps` function receives all region contexts in the order `join` functions are given.
## Transient region
In some cases we don't need a react's context to hold our state and workflows, the `withTransientRegion` HOC defines a region only for given component, it is a useful utility to separate state management from presetation.
```jsx
import {withTransientRegion} from 'react-kiss';
const initialState = {
value: 0
};
const workflows = {
increment(payload, {value}) {
return {value: value + 1};
},
decrement(payload, {value}) {
return {value: value - 1};
}
};
// The Counter component now is a pure presentational function component, state and workflows are defined in region
const Counter = ({value, increment, decrement}) => (
dec
{value}
inc
);
export default withTransientRegion(initialState, workflows)(Counter);
```
## Specific regions
`react-kiss` also provides some predefined regions to handle common scenarios.
### Query
The `defineQueryRegion` function accepts a request function and defines a region in such structure:
```javascript
{
queries: {
[stringifiedParams]: {
pendingMutex: 0, // The number of on-the-way request
params: {}, // Requesting params
response: {
data: {}, // Response of success request
error: {} // Response of fail request
}
},
...
},
request: function, // The workflow to trigger request
findQuery: function(params), // Selector to find query object by params
findReponse: function(params), // Selector to find query.response object by params
findData: function(params) // Selector to find query.response.data object by params
}
```
For each invocation of `request` workflow, a `[stringifiedParams]: Query` key-value pair is stored in `queries` state.