updux/docs/tutorial.md

408 lines
8.9 KiB
Markdown
Raw Normal View History

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