updux/docs/tutorial.md

422 lines
9.1 KiB
Markdown
Raw Normal View History

2020-06-02 20:00:48 +00:00
# Tutorial
2020-06-19 23:29:12 +00:00
This tutorial walks you through the features of `Updux` using the
2020-06-02 20:00:48 +00:00
time-honored example of the implementation of Todo list store.
2021-10-12 22:13:59 +00:00
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
plain JavaScript.
2020-06-02 20:00:48 +00:00
## Definition of the state
2021-10-12 22:13:59 +00:00
To begin with, let's define that has nothing but an initial state.
2020-06-02 20:00:48 +00:00
2021-10-12 22:13:59 +00:00
```js
import { Updux } from 'updux';
2020-06-02 20:00:48 +00:00
2021-10-12 22:13:59 +00:00
const todosDux = new Updux({
2020-06-02 20:00:48 +00:00
initial: {
next_id: 1,
todos: [],
2021-10-12 22:13:59 +00:00
}
2020-06-02 20:00:48 +00:00
});
```
2021-10-12 22:13:59 +00:00
Congrats! You have written your first Updux object. It
2020-06-02 20:00:48 +00:00
doesn't do a lot, but you can already create a store out of it, and its
initial state will be automatically set:
2021-10-12 22:13:59 +00:00
```js
const store = todosDux.createStore();
2020-06-02 20:00:48 +00:00
2021-10-12 22:13:59 +00:00
console.log(store.getState()); // prints { next_id: 1, todos: [] }
2020-06-02 20:00:48 +00:00
```
## Add actions
This is all good, but a little static. Let's add actions!
2021-10-12 22:13:59 +00:00
```js
todosDux.setAction( 'addTodo' );
todosDux.setAction( 'todoDone' );
2020-06-02 20:00:48 +00:00
```
### Accessing actions
2020-06-19 23:29:12 +00:00
Once an action is defined, its creator is accessible via the `actions` accessor.
2020-06-02 20:00:48 +00:00
2021-10-13 17:54:17 +00:00
```js
console.log( todosDux.actions.addTodo('write tutorial') ); // prints { type: 'addTodo', payload: 'write tutorial' }
2020-06-02 20:00:48 +00:00
```
### Adding a mutation
2021-10-13 17:54:17 +00:00
Like actions, a mutation can be defined as part of the Updux
2020-06-02 20:00:48 +00:00
init arguments:
2021-10-13 17:54:17 +00:00
```js
const todosDux = new Updux({
actions: {
addTodo: null
},
mutations: {
addTodo: description => ({next_id: id, todos}) => ({
next_id: 1 + id,
todos: [...todos, { description, id, done: false }]
})
2020-06-02 20:00:48 +00:00
}
});
```
2021-10-13 17:54:17 +00:00
or via the method `setMutation`:
2020-06-02 20:00:48 +00:00
2021-10-13 17:54:17 +00:00
```js
todosDux.setMutation( 'addTodo', description => ({next_id: id, todos}) => ({
next_id: 1 + id,
todos: [...todos, { description, id, done: false }]
}));
2020-06-02 20:00:48 +00:00
```
## Effects
2020-06-03 15:36:26 +00:00
In addition to mutations, Updux also provides action-specific middleware, here
2020-06-02 20:00:48 +00:00
called effects.
2021-10-13 23:31:37 +00:00
Effects use the usual Redux middleware signature, plus a few goodies.
The `getState` and `dispatch` functions are augmented with the dux selectors,
and actions, respectively. The selectors and actions are also available by
themselves in the api object too.
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
```js
2020-06-02 20:00:48 +00:00
import u from 'updeep';
2020-06-19 23:29:12 +00:00
// we want to decouple the increment of next_id and the creation of
2021-10-13 23:31:37 +00:00
// a new todo. So let's use a new version of the action 'addTodo'.
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
const addTodoWithId = action('addTodoWithId');
const incNextId = action('incNextId');
const addTodo = action('addTodo');
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
const addTodoEffect = ({ getState, dispatch }) => next => action => {
const id = getState.nextId();
dispatch.incNextId();
2020-06-02 20:00:48 +00:00
next(action);
2021-10-13 23:31:37 +00:00
dispatch.addTodoWithId({ description: action.payload, id }) );
2020-06-02 20:00:48 +00:00
}
```
And just like mutations, they can be defined as part of the init
configuration, or after via the method `addEffect`:
```
2021-10-13 23:31:37 +00:00
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');
2020-06-02 20:00:48 +00:00
```
or
```
2021-10-13 23:31:37 +00:00
todosDux.addEffect( 'addTodo', addTodoEffect );
2020-06-02 20:00:48 +00:00
```
### Catch-all effect
2020-06-19 23:29:12 +00:00
It is possible to have an effect match all actions via the special `*` token.
2020-06-02 20:00:48 +00:00
```
todosUpdux.addEffect('*', () => next => action => {
console.log( 'seeing action fly by:', action );
next(action);
});
```
2021-10-13 23:31:37 +00:00
## Adding selectors
2020-06-02 20:00:48 +00:00
Selectors can be defined to get data derived from the state.
From now you should know the drill: selectors can be defined at construction
2021-10-13 23:31:37 +00:00
time or via `setSelector`.
2020-06-02 20:00:48 +00:00
```
import fp from 'lodash/fp';
const getTodoById = ({todos}) => id => fp.find({id},todos);
const todosUpdux = new Updux({
selectors: {
2020-06-19 23:29:12 +00:00
getTodoById
2020-06-02 20:00:48 +00:00
}
})
```
2020-06-19 23:29:12 +00:00
or
2020-06-02 20:00:48 +00:00
```
2021-10-13 23:31:37 +00:00
todosDux.setSelector('getTodoById', ({todos}) => id => fp.find({id},todos));
2020-06-02 20:00:48 +00:00
```
### Accessing selectors
2021-10-13 23:31:37 +00:00
The `getState` method of a dux store is augmented
with its selectors, with the first call for the state already
called in for you.
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
```js
const store = todosDux.createStore();
2020-06-02 20:00:48 +00:00
2020-06-19 23:29:12 +00:00
console.log(
2021-10-13 23:31:37 +00:00
todosUpdux.getState.getTodoById(1)
2020-06-02 20:00:48 +00:00
);
```
## Subduxes
2021-10-13 23:31:37 +00:00
Now that we have all the building blocks, we can embark on the last and
funkiest part of Updux: its recursive nature.
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
### Recap: the Todos dux, undivided
2020-06-02 20:00:48 +00:00
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
2021-10-13 23:31:37 +00:00
let's recap on the Todos dux we have so far:
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
```js
2020-06-02 20:00:48 +00:00
import Updux from 'updux';
import u from 'updeep';
import fp from 'lodash/fp';
2021-10-13 23:31:37 +00:00
const todosDux = new Updux({
2020-06-02 20:00:48 +00:00
initial: {
2021-10-13 23:31:37 +00:00
nextId: 1,
2020-06-02 20:00:48 +00:00
todos: [],
2021-10-13 23:31:37 +00:00
},
2020-06-02 20:00:48 +00:00
actions: {
2021-10-13 23:31:37 +00:00
addTodo: null,
addTodoWithId: (description, id) => ({description, id, done: false}),
todoDone: null,
incNextId: null,
2020-06-02 20:00:48 +00:00
},
selectors: {
getTodoById: ({todos}) => id => fp.find({id},todos)
2021-10-13 23:31:37 +00:00
},
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();
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
dispatch.incNextId();
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
next(action);
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
dispatch.addTodoWithId(action.payload, id);
}
}
2020-06-02 20:00:48 +00:00
});
```
2021-10-13 23:31:37 +00:00
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
2020-06-02 20:00:48 +00:00
create upduxes for each of those.
2021-10-13 23:31:37 +00:00
### NextId dux
2020-06-02 20:00:48 +00:00
```
2021-10-13 23:31:37 +00:00
// dux/nextId.js
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
import { Updux } from 'updux';
2020-06-02 20:00:48 +00:00
import u from 'updeep';
import fp from 'lodash/fp';
2021-10-13 23:31:37 +00:00
export default new Updux({
2020-06-02 20:00:48 +00:00
initial: 1,
actions: {
2021-10-13 23:31:37 +00:00
incNextId: null,
2020-06-02 20:00:48 +00:00
},
selectors: {
getNextId: state => state
2021-10-13 23:31:37 +00:00
},
mutations: {
incrementNextId: () => fp.add(1)
2020-06-02 20:00:48 +00:00
}
});
```
### Todo updux
```
// dux/todos/todo/index.ts
2021-10-13 23:31:37 +00:00
import { Updux } from 'updux';
2020-06-02 20:00:48 +00:00
import u from 'updeep';
import fp from 'lodash/fp';
2021-10-13 23:31:37 +00:00
export default new Updux({
2020-06-02 20:00:48 +00:00
initial: {
2021-10-13 23:31:37 +00:00
id: 0,
2020-06-02 20:00:48 +00:00
description: "",
done: false,
2021-10-13 23:31:37 +00:00
},
2020-06-02 20:00:48 +00:00
actions: {
2021-10-13 23:31:37 +00:00
todoDone: null,
},
mutations: {
todoDone: id => u.if( fp.matches({id}), { done: true }) )
2020-06-02 20:00:48 +00:00
}
});
```
### Todos updux
```
2021-10-13 23:31:37 +00:00
// dux/todos/index.js
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
import { Updux } from 'updux';
2020-06-02 20:00:48 +00:00
import u from 'updeep';
import fp from 'lodash/fp';
2021-10-13 23:31:37 +00:00
import todo from './todo/index.js';
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
export default new Updux({
initial: [],
2020-06-02 20:00:48 +00:00
subduxes: {
2021-10-13 23:31:37 +00:00
'*': todo
2020-06-02 20:00:48 +00:00
},
actions: {
2021-10-13 23:31:37 +00:00
addTodoWithId,
2020-06-02 20:00:48 +00:00
},
2021-10-13 23:31:37 +00:00
mappedSelectors: {
2020-06-02 20:00:48 +00:00
getTodoById: state => id => fp.find({id},state)
2021-10-13 23:31:37 +00:00
},
mutations: {
addTodoWithId: todo =>
todos => [ ...todos, todo ]
2020-06-02 20:00:48 +00:00
}
});
```
2020-06-19 23:29:12 +00:00
Note the special '\*' subdux key used here. This
2020-06-02 20:00:48 +00:00
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
```
2021-10-13 23:31:37 +00:00
// dux/index.js
2020-06-02 20:00:48 +00:00
import Updux from 'updux';
import todos from './todos';
2021-10-13 23:31:37 +00:00
import nextId from './next_id';
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
export new Updux({
2020-06-02 20:00:48 +00:00
subduxes: {
2021-10-13 23:31:37 +00:00
nextId,
2020-06-02 20:00:48 +00:00
todos,
},
actions: {
2021-10-13 23:31:37 +00:00
addTodo: null
},
effects: {
addTodo: ({ getState, dispatch }) => next => action => {
const id = getState.getNextId();
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
dispatch.incNextId()
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
next(action);
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
dispatch.addTodoWithId( action.payload, id );
}
}
2020-06-02 20:00:48 +00:00
});
```
2021-10-13 23:31:37 +00:00
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`
2020-06-19 23:29:12 +00:00
action from the `todos`.
2020-06-02 20:00:48 +00:00
2020-06-03 15:36:26 +00:00
Note that the `getNextId` selector still gets the
right value; when aggregating subduxes selectors Updux auto-wraps them to
2021-10-13 23:31:37 +00:00
access the right slice of the top object. ```
2020-06-02 20:00:48 +00:00
2021-10-13 23:31:37 +00:00
## Reactions
2020-06-19 23:29:12 +00:00
2021-10-13 23:31:37 +00:00
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:
2020-06-19 23:29:12 +00:00
```
2021-10-13 23:31:37 +00:00
(storeApi) => (state, previousState, unsubscribe) => {
2020-06-19 23:29:12 +00:00
...
}
```
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:
```
2021-10-13 23:31:37 +00:00
const todos = new Updux({
2020-06-19 23:29:12 +00:00
initial: [],
2021-10-13 23:31:37 +00:00
actions: {
setNbrTodos: null,
}
reactions: [
({dispatch}) => todos => dispatch.setNbrTodos(todos.length);
2020-06-19 23:29:12 +00:00
],
});
2021-10-13 23:31:37 +00:00
const myDux = new Updux({
2020-06-19 23:29:12 +00:00
initial: {
nbr_todos: 0
},
subduxes: {
todos,
},
2021-10-13 23:31:37 +00:00
mutations: {
setNbrTodos: nbrTodos => u({ nbrTodos })
}
2020-06-19 23:29:12 +00:00
})
```
2020-06-02 20:00:48 +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