2022-08-26 19:51:00 +00:00
|
|
|
# Tutorial
|
|
|
|
|
|
|
|
This tutorial walks you through the features of `Updux` using the
|
|
|
|
time-honored example of the implementation of Todo list store.
|
|
|
|
|
|
|
|
We'll be using
|
|
|
|
[updeep](https://www.npmjs.com/package/updeep) to
|
|
|
|
help with immutability and deep merging,
|
|
|
|
but that's totally optional. If `updeep` is not your bag,
|
|
|
|
it can easily be substitued with, say, [immer][], [lodash][], or even
|
2022-08-29 14:58:40 +00:00
|
|
|
plain JavaScript.
|
2022-08-26 19:51:00 +00:00
|
|
|
|
|
|
|
## Definition of the state
|
|
|
|
|
|
|
|
To begin with, let's define that has nothing but an initial state.
|
|
|
|
|
|
|
|
```js
|
|
|
|
import { Updux } from 'updux';
|
|
|
|
|
|
|
|
const todosDux = new Updux({
|
|
|
|
initial: {
|
|
|
|
next_id: 1,
|
|
|
|
todos: [],
|
|
|
|
}
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
Congrats! You have written your first Updux object. It
|
|
|
|
doesn't do a lot, but you can already create a store out of it, and its
|
|
|
|
initial state will be automatically set:
|
|
|
|
|
|
|
|
```js
|
|
|
|
const store = todosDux.createStore();
|
|
|
|
|
|
|
|
console.log(store.getState()); // prints { next_id: 1, todos: [] }
|
|
|
|
```
|
|
|
|
|
|
|
|
## Add actions
|
|
|
|
|
|
|
|
This is all good, but a little static. Let's add actions!
|
|
|
|
|
|
|
|
```js
|
|
|
|
const todosDux = new Updux({
|
|
|
|
initial: {
|
|
|
|
next_id: 1,
|
|
|
|
todos: [],
|
2022-08-29 14:58:40 +00:00
|
|
|
},
|
2022-08-26 19:51:00 +00:00
|
|
|
{
|
|
|
|
addTodo: null,
|
|
|
|
todoDone: null,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
### Accessing actions
|
|
|
|
|
|
|
|
Once an action is defined, its creator is accessible via the `actions` accessor.
|
|
|
|
|
|
|
|
```js
|
2022-08-29 14:58:40 +00:00
|
|
|
console.log( todosDux.actions.addTodo('write tutorial') );
|
2022-08-26 19:51:00 +00:00
|
|
|
// prints { type: 'addTodo', payload: 'write tutorial' }
|
|
|
|
```
|
2022-08-28 16:54:14 +00:00
|
|
|
### Adding a mutation
|
|
|
|
|
|
|
|
Mutations are the reducing functions associated to actions. They
|
|
|
|
are defined via the `setMutation` method:
|
|
|
|
|
|
|
|
|
|
|
|
```js
|
|
|
|
todosDux.setMutation( 'addTodo', description => ({next_id: id, todos}) => ({
|
|
|
|
next_id: 1 + id,
|
|
|
|
todos: [...todos, { description, id, done: false }]
|
|
|
|
}));
|
|
|
|
```
|
2022-08-28 23:29:31 +00:00
|
|
|
## Effects
|
|
|
|
|
|
|
|
In addition to mutations, Updux also provides action-specific middleware, here
|
|
|
|
called effects.
|
|
|
|
|
|
|
|
Effects use the usual Redux middleware signature, plus a few goodies.
|
|
|
|
The `getState` and `dispatch` functions are augmented with the dux selectors,
|
2022-08-29 14:58:40 +00:00
|
|
|
and actions, respectively. The selectors and actions are also available
|
2022-08-28 23:29:31 +00:00
|
|
|
from the api object.
|
|
|
|
|
|
|
|
```js
|
|
|
|
import u from 'updeep';
|
|
|
|
import { action, Updux } from 'updux';
|
|
|
|
|
|
|
|
// we want to decouple the increment of next_id and the creation of
|
|
|
|
// a new todo. So let's use a new version of the action 'addTodo'.
|
|
|
|
|
|
|
|
const addTodoWithId = action('addTodoWithId');
|
|
|
|
const incNextId = action('incNextId');
|
|
|
|
const addTodo = action('addTodo');
|
|
|
|
|
|
|
|
const addTodoEffect = ({ getState, dispatch }) => next => action => {
|
|
|
|
const id = getState.nextId();
|
|
|
|
|
|
|
|
dispatch.incNextId();
|
|
|
|
|
|
|
|
next(action);
|
|
|
|
|
|
|
|
dispatch.addTodoWithId({ description: action.payload, id });
|
|
|
|
}
|
|
|
|
|
|
|
|
const todosDux = new Updux({
|
|
|
|
initial: { nextId: 1, todos: [] },
|
|
|
|
actions: { addTodo, incNextId, addTodoWithId },
|
|
|
|
selectors: {
|
|
|
|
nextId: ({nextId}) => nextId,
|
|
|
|
},
|
|
|
|
mutations: {
|
|
|
|
addTodoWithId: (todo) => u({ todos: (todos) => [...todos, todo] }),
|
|
|
|
incNextId: () => u({ nextId: id => id+1 }),
|
|
|
|
},
|
|
|
|
effects: {
|
|
|
|
'addTodo': addTodoEffect
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const store = todosDux.createStore();
|
|
|
|
|
|
|
|
store.dispatch.addTodo('Do the thing');
|
|
|
|
```
|
|
|
|
|
2022-08-29 14:57:59 +00:00
|
|
|
### Catch-all effect
|
|
|
|
|
|
|
|
It is possible to have an effect match all actions via the special `*` token.
|
2022-08-29 15:56:30 +00:00
|
|
|
|
2022-08-29 14:57:59 +00:00
|
|
|
```
|
|
|
|
todosUpdux.addEffect('*', () => next => action => {
|
|
|
|
console.log( 'seeing action fly by:', action );
|
|
|
|
next(action);
|
|
|
|
});
|
2022-08-29 15:56:30 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
## Adding selectors
|
|
|
|
|
|
|
|
Selectors can be defined to get data derived from the state.
|
|
|
|
From now you should know the drill: selectors can be defined at construction
|
|
|
|
time or via `setSelector`.
|
|
|
|
|
|
|
|
```
|
|
|
|
const getTodoById = ({todos}) => targetId => todos.find(({id}) => id === targetId);
|
|
|
|
|
|
|
|
const todosUpdux = new Updux({
|
|
|
|
selectors: {
|
|
|
|
getTodoById
|
|
|
|
}
|
|
|
|
})
|
|
|
|
```
|
|
|
|
|
|
|
|
or
|
|
|
|
|
|
|
|
```
|
|
|
|
todosDux.setSelector('getTodoById', getTodoById);
|
|
|
|
```
|
|
|
|
|
|
|
|
### Accessing selectors
|
|
|
|
|
|
|
|
The `getState` method of a dux store is augmented
|
|
|
|
with its selectors, with the first call for the state already
|
|
|
|
called in for you.
|
|
|
|
|
|
|
|
```js
|
|
|
|
const store = todosDux.createStore();
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
todosUpdux.getState.getTodoById(1)
|
|
|
|
);
|
|
|
|
```
|
|
|
|
|
2022-08-30 15:00:00 +00:00
|
|
|
## Subduxes
|
|
|
|
|
|
|
|
Now that we have all the building blocks, we can embark on the last and
|
|
|
|
funkiest part of Updux: its recursive nature.
|
|
|
|
|
|
|
|
### Recap: the Todos dux, undivided
|
|
|
|
|
|
|
|
Upduxes can be divided into sub-upduxes that deal with the various parts of
|
|
|
|
the global state. This is better understood by working out an example, so
|
|
|
|
let's recap on the Todos dux we have so far:
|
|
|
|
|
|
|
|
```js
|
|
|
|
import Updux from 'updux';
|
|
|
|
import u from 'updeep';
|
|
|
|
import fp from 'lodash/fp';
|
|
|
|
|
|
|
|
const todosDux = new Updux({
|
|
|
|
initial: {
|
|
|
|
nextId: 1,
|
|
|
|
todos: [],
|
|
|
|
},
|
|
|
|
actions: {
|
|
|
|
addTodo: null,
|
|
|
|
addTodoWithId: (description, id) => ({description, id, done: false}),
|
|
|
|
todoDone: null,
|
|
|
|
incNextId: null,
|
|
|
|
},
|
|
|
|
selectors: {
|
|
|
|
getTodoById: ({todos}) => id => fp.find({id},todos)
|
|
|
|
},
|
|
|
|
mutations: {
|
|
|
|
addTodoWithId: todo =>
|
|
|
|
u.updateIn( 'todos', todos => [ ...todos, todo] ),
|
|
|
|
incrementNextId: () => u({ nextId: fp.add(1) }),
|
|
|
|
todoDone: (id) => u.updateIn('todos',
|
|
|
|
u.map( u.if( fp.matches({id}), todo => u({done: true}, todo) ) )
|
|
|
|
),
|
|
|
|
},
|
|
|
|
effects: {
|
|
|
|
addTodo: ({ getState, dispatch }) => next => action => {
|
|
|
|
const { nextId: id } = getState();
|
|
|
|
|
|
|
|
dispatch.incNextId();
|
|
|
|
|
|
|
|
next(action);
|
|
|
|
|
|
|
|
dispatch.addTodoWithId(action.payload, id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
This store has two main components: the `nextId`, and the `todos` collection.
|
|
|
|
The `todos` collection is itself composed of the individual `todo`s. Let's
|
|
|
|
create upduxes for each of those.
|
|
|
|
|
|
|
|
### NextId dux
|
|
|
|
|
|
|
|
```
|
|
|
|
// dux/nextId.js
|
|
|
|
|
|
|
|
import { Updux } from 'updux';
|
|
|
|
import u from 'updeep';
|
|
|
|
|
|
|
|
export default new Updux({
|
|
|
|
initial: 1,
|
|
|
|
actions: {
|
|
|
|
incrementNextId: null,
|
|
|
|
},
|
|
|
|
selectors: {
|
|
|
|
getNextId: state => state
|
|
|
|
},
|
|
|
|
mutations: {
|
|
|
|
incrementNextId: () => state => state + 1,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
### Todo updux
|
|
|
|
|
|
|
|
```
|
|
|
|
// dux/todos/todo/index.ts
|
|
|
|
|
|
|
|
import { Updux } from 'updux';
|
|
|
|
import u from 'updeep';
|
|
|
|
import fp from 'lodash/fp';
|
|
|
|
|
|
|
|
export default new Updux({
|
|
|
|
initial: {
|
|
|
|
id: 0,
|
|
|
|
description: "",
|
|
|
|
done: false,
|
|
|
|
},
|
|
|
|
actions: {
|
|
|
|
todoDone: null,
|
|
|
|
},
|
|
|
|
mutations: {
|
|
|
|
todoDone: id => u.if( fp.matches({id}), { done: true }) )
|
|
|
|
},
|
|
|
|
selectors: {
|
|
|
|
desc: ({description}) => description,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
### Todos updux
|
|
|
|
|
|
|
|
```
|
|
|
|
// dux/todos/index.js
|
|
|
|
|
|
|
|
import { Updux } from 'updux';
|
|
|
|
import u from 'updeep';
|
|
|
|
import fp from 'lodash/fp';
|
|
|
|
|
|
|
|
import todo from './todo/index.js';
|
|
|
|
|
|
|
|
export default new Updux({
|
|
|
|
initial: [],
|
|
|
|
subduxes: {
|
|
|
|
'*': todoDux
|
|
|
|
},
|
|
|
|
actions: {
|
|
|
|
addTodoWithId: (description, id) => ({description, id} )
|
|
|
|
},
|
|
|
|
findSelectors: {
|
|
|
|
getTodoById: state => id => fp.find({id},state)
|
|
|
|
},
|
|
|
|
mutations: {
|
|
|
|
addTodoWithId: todo =>
|
|
|
|
todos => [ ...todos, todo ]
|
|
|
|
}
|
|
|
|
});
|
|
|
|
```
|
|
|
|
|
|
|
|
Note the special '\*' subdux key used here. This
|
|
|
|
allows the updux to map every item present in its
|
|
|
|
state to a `todo` updux. See [this recipe](/recipes?id=mapping-a-mutation-to-all-values-of-a-state) for details.
|
|
|
|
|
|
|
|
### Main store
|
|
|
|
|
|
|
|
```
|
|
|
|
// dux/index.js
|
|
|
|
|
|
|
|
import Updux from 'updux';
|
|
|
|
|
|
|
|
import todos from './todos';
|
|
|
|
import nextId from './next_id';
|
|
|
|
|
|
|
|
export new Updux({
|
|
|
|
subduxes: {
|
|
|
|
nextId,
|
|
|
|
todos,
|
|
|
|
},
|
|
|
|
actions: {
|
|
|
|
addTodo: null
|
|
|
|
},
|
|
|
|
effects: {
|
|
|
|
addTodo: ({ getState, dispatch }) => next => action => {
|
|
|
|
const id = getState.getNextId();
|
|
|
|
|
|
|
|
dispatch.incrementNextId()
|
|
|
|
|
|
|
|
next(action);
|
|
|
|
|
|
|
|
dispatch.addTodoWithId( action.payload, id );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
Tadah! We had to define the `addTodo` effect at the top level as it needs to
|
|
|
|
access the `getNextId` selector from `nextId` and the `addTodoWithId`
|
|
|
|
action from the `todos`.
|
|
|
|
|
|
|
|
Note that the `getNextId` selector still gets the
|
|
|
|
right value; when aggregating subduxes selectors Updux auto-wraps them to
|
|
|
|
access the right slice of the top object. ```
|
|
|
|
|
2022-08-30 18:15:47 +00:00
|
|
|
## Reactions
|
|
|
|
|
|
|
|
Reactions -- aka Redux's subscriptions -- can be added to a updux store via the initial config
|
|
|
|
or the method `addSubscription`. The signature of a reaction is:
|
|
|
|
|
|
|
|
```
|
|
|
|
(storeApi) => (state, previousState, unsubscribe) => {
|
|
|
|
...
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
Subscriptions registered for an updux and its subduxes are automatically
|
|
|
|
subscribed to the store when calling `createStore`.
|
|
|
|
|
|
|
|
The `state` passed to the subscriptions of the subduxes is the local state.
|
|
|
|
|
|
|
|
Also, all subscriptions are wrapped such that they are called only if the
|
|
|
|
local `state` changed since their last invocation.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
```
|
|
|
|
const todos = new Updux({
|
|
|
|
initial: [],
|
|
|
|
actions: {
|
|
|
|
setNbrTodos: null,
|
2022-08-30 18:32:56 +00:00
|
|
|
addTodo: null,
|
|
|
|
},
|
|
|
|
mutations: {
|
|
|
|
addTodo: todo => todos => [ ...todos, todo ],
|
|
|
|
},
|
2022-08-30 18:15:47 +00:00
|
|
|
reactions: [
|
2022-08-30 18:32:56 +00:00
|
|
|
({dispatch}) => todos => dispatch.setNbrTodos(todos.length)
|
2022-08-30 18:15:47 +00:00
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const myDux = new Updux({
|
|
|
|
initial: {
|
2022-08-30 18:32:56 +00:00
|
|
|
nbrTodos: 0
|
2022-08-30 18:15:47 +00:00
|
|
|
},
|
|
|
|
subduxes: {
|
|
|
|
todos,
|
|
|
|
},
|
|
|
|
mutations: {
|
|
|
|
setNbrTodos: nbrTodos => u({ nbrTodos })
|
|
|
|
}
|
2022-08-30 18:32:56 +00:00
|
|
|
});
|
2022-08-30 18:15:47 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
[immer]: https://www.npmjs.com/package/immer
|
|
|
|
[lodash]: https://www.npmjs.com/package/lodash
|
|
|
|
[ts-action]: https://www.npmjs.com/package/ts-action
|
2022-08-29 15:56:30 +00:00
|
|
|
|