updux/docs/tutorial.md

508 lines
12 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.
Effects use the usual Redux middleware signature:
```
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
2020-06-02 20:00:48 +00:00
// a new todo. So let's use a new version of the action 'add_todo'.
const add_todo_with_id = action('add_todo_with_id', payload<{description: string; id?: number}>() );
const inc_next_id = action('inc_next_id');
2020-06-19 23:29:12 +00:00
const populate_next_id = ({ getState, dispatch }) => next => action => {
2020-06-02 20:00:48 +00:00
const { next_id: id } = getState();
dispatch(inc_next_id());
next(action);
dispatch( add_todo_with_id({ description: action.payload, id }) );
}
```
And just like mutations, they can be defined as part of the init
configuration, or after via the method `addEffect`:
```
const todosUpdux = new Updux({
actions: { add_todo, inc_next_id },
effects: [
[ add_todo, populate_next_id ]
]
})
```
or
```
const todosUpdux = new Updux({
actions: { add_todo, inc_next_id },
});
todosUpdux.addEffect( add_todo, populate_next_id );
```
2020-06-19 23:29:12 +00:00
As for the mutations, for TypeScript projects
the use of `addEffect` is prefered, as the method gives visibility to the
2020-06-02 20:00:48 +00:00
action and state types.
### 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);
});
```
## Selectors
Selectors can be defined to get data derived from the state.
### Adding selectors
From now you should know the drill: selectors can be defined at construction
time or via `addSelector`.
```
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
```
todosUpdux.addSelector('getTodoById', ({todos}) => id => fp.find({id},todos));
```
Here the declaration as part of the constructor configuration is prefered.
2020-06-03 15:36:26 +00:00
Whereas the `addSelector` will provide the state's type as part of its
2020-06-02 20:00:48 +00:00
signature, declaring the selectors via the constructors will make them visible
via the type of the accessors `selectors`.
### Accessing selectors
Selectors are available via the accessor `selectors`.
```
const store = todosUpdux.createStore();
2020-06-19 23:29:12 +00:00
console.log(
2020-06-02 20:00:48 +00:00
todosUpdux.selectors.getTodoById( store.getState() )(1)
);
```
## Subduxes
Now that we have all the building blocks, we can embark on the last, and best,
part of Updux: its recursive nature.
### Recap: the Todos updux, 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 Updux we have so far:
```
import Updux from 'updux';
import { action, payload } from 'ts-action';
import u from 'updeep';
import fp from 'lodash/fp';
type Todo = {
id: number;
description: string;
done: boolean;
};
type TodoStore = {
next_id: number;
todos: Todo[];
};
const add_todo = action('add_todo', payload<string>() );
2020-06-19 23:29:12 +00:00
const add_todo_with_id = action('add_todo_with_id',
2020-06-02 20:00:48 +00:00
payload<{ description: string; id: number }>() );
const todo_done = action('todo_done', payload<number>() );
2020-06-19 23:29:12 +00:00
const increment_next_id = action('increment_next_id');
2020-06-02 20:00:48 +00:00
const todosUpdux = new Updux({
initial: {
next_id: 1,
todos: [],
} as TodoStore,
actions: {
add_todo,
add_todo_with_id,
todo_done,
2020-06-03 15:36:26 +00:00
increment_next_id,
2020-06-02 20:00:48 +00:00
},
selectors: {
getTodoById: ({todos}) => id => fp.find({id},todos)
}
});
2020-06-19 23:29:12 +00:00
todosUpdux.addMutation( add_todo_with_id, payload =>
2020-06-02 20:00:48 +00:00
u.updateIn( 'todos', todos => [ ...todos, { ...payload, done: false }] )
);
2020-06-03 15:36:26 +00:00
todosUpdux.addMutation( increment_next_id, () => u({ next_id: i => i + 1 }) );
2020-06-02 20:00:48 +00:00
todosUpdux.addMutation( todo_done, id => u.updateIn(
'todos', u.map( u.if( fp.matches({id}), todo => u({done: true}, todo) ) )
2020-06-19 23:29:12 +00:00
) );
2020-06-02 20:00:48 +00:00
2020-06-19 23:29:12 +00:00
todosUpdux.addEffect( add_todo, ({ getState, dispatch }) => next => action => {
2020-06-02 20:00:48 +00:00
const { next_id: id } = getState();
dispatch(inc_next_id());
next(u.updateIn('payload', {id}, action))
});
```
This store has two main components: the `next_id`, and the `todos` collection.
The `todos` collection is itself composed of the individual `todo`s. So let's
create upduxes for each of those.
### Next_id updux
```
// dux/next_id.ts
import Updux from 'updux';
import { action, payload } from 'ts-action';
import u from 'updeep';
import fp from 'lodash/fp';
2020-06-03 15:36:26 +00:00
const increment_next_id = action('increment_next_id');
2020-06-02 20:00:48 +00:00
const updux = new Updux({
initial: 1,
actions: {
2020-06-03 15:36:26 +00:00
increment_next_id,
2020-06-02 20:00:48 +00:00
},
selectors: {
getNextId: state => state
}
});
2020-06-03 15:36:26 +00:00
updux.addMutation( increment_next_id, () => fp.add(1) );
2020-06-02 20:00:48 +00:00
export default updux.asDux;
```
2020-06-03 15:36:26 +00:00
Notice that we didn't have to specify the type of `initial`;
2020-06-02 20:00:48 +00:00
TypeScript figures by itself that it's a number.
Also, note that we're exporting the output of the accessor `asDux` instead of
the updux object itself. See the upcoming section 'Exporting upduxes' for the rationale.
### Todo updux
```
// dux/todos/todo/index.ts
import Updux from 'updux';
import { action, payload } from 'ts-action';
import u from 'updeep';
import fp from 'lodash/fp';
type Todo = {
id: number;
description: string;
done: boolean;
};
const todo_done = action('todo_done', payload<number>() );
const updux = new Updux({
initial: {
next_id: 0,
description: "",
done: false,
} as Todo,
actions: {
todo_done
}
});
updux.addMutation( todo_done, id => u.if( fp.matches({id}), { done: true }) );
export default updux.asDux;
```
### Todos updux
```
// dux/todos/index.ts
import Updux, { DuxState } from 'updux';
import { action, payload } from 'ts-action';
import u from 'updeep';
import fp from 'lodash/fp';
import todo from './todo';
type TodoState = DuxState<typeof todo>;
2020-06-19 23:29:12 +00:00
const add_todo_with_id = action('add_todo_with_id',
payload<{ description: string; id: number }>()
2020-06-02 20:00:48 +00:00
);
const updux = new Updux({
initial: [] as Todo[],
subduxes: {
2020-06-19 23:29:12 +00:00
'*': todo.upreducer
2020-06-02 20:00:48 +00:00
},
actions: {
add_todo_with_id,
},
selectors: {
getTodoById: state => id => fp.find({id},state)
}
});
2020-06-19 23:29:12 +00:00
todosUpdux.addMutation( add_todo_with_id, payload =>
2020-06-02 20:00:48 +00:00
todos => [ ...todos, { ...payload, done: false }]
);
export default updux.asDux;
```
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.
We could also have written the updux as:
```
const updux = new Updux({
initial: [] as Todo[],
actions: {
add_todo_with_id,
},
selectors: {
getTodoById: state => id => fp.find({id},state)
},
mutations: {
'*': (payload,action) => state => u.map( todo.reducer(state, action) )
}
});
```
Note how we are using the `upreducer` accessor in the first case (which yields
2020-06-19 23:29:12 +00:00
a reducer for the dux using the signature `(payload,action) => state => new_state`) and `reducer` in the second case (which yield an equivalent
2020-06-02 20:00:48 +00:00
reducer using the classic signature `(state,action) => new_state`).
### Main store
```
// dux/index.ts
import Updux from 'updux';
import todos from './todos';
import next_id from './next_id';
const add_todo = action('add_todo', payload<string>() );
const updux = new Updux({
subduxes: {
next_id,
todos,
},
actions: {
add_todo
}
});
2020-06-19 23:29:12 +00:00
todos.addEffect( add_todo, ({ getState, dispatch }) => next => action => {
2020-06-02 20:00:48 +00:00
const id = updux.selectors.getNextId( getState() );
dispatch(updux.actions.inc_next_id());
next(action);
dispatch( updux.actions.add_todo_with_id({ description: action.payload, id }) );
});
export default updux.asDux;
```
2020-06-19 23:29:12 +00:00
Tadah! We had to define the `add_todo` effect at the top level as it needs to
2020-06-02 20:00:48 +00:00
access the `getNextId` selector from `next_id` and the `add_todo_with_id`
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
access the right slice of the top object. i.e., the `getNextId` selector
2020-06-02 20:00:48 +00:00
at the main level is actually defined as:
```
const getNextId = state => next_id.selectors.getNextId(state.next_id);
```
2020-06-19 23:29:12 +00:00
## Subscriptions
Subscriptions can be added by default to a updux store via the initial config
or the method `addSubscription`. The signature of a subscription is:
```
(store) => (state,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 set_nbr_todos = action('set_nbr_todos', payload() );
const todos = dux({
initial: [],
subscriptions: [
({dispatch}) => todos => dispatch(set_nbr_todos(todos.length))
],
});
const myDux = dux({
initial: {
nbr_todos: 0
},
subduxes: {
todos,
},
mutations: [
[ set_nbr_todos, nbr_todos => u({nbr_todos}) ]
]
})
```
2020-06-02 20:00:48 +00:00
## Exporting upduxes
As a general rule, don't directly export your upduxes, but rather use the accessor `asDux`.
```
const updux = new Updux({ ... });
...
export default updux.asDux;
```
`asDux` returns an immutable copy of the attributes of the updux. Exporting
this instead of the updux itself prevents unexpected modifications done
outside of the updux declaration file. More importantly, the output of
2020-06-19 23:29:12 +00:00
`asDux` has more precise typing, which in result results in better typing of
2020-06-02 20:00:48 +00:00
parent upduxes using the dux as one of its subduxes.
[immer]: https://www.npmjs.com/package/immer
[lodash]: https://www.npmjs.com/package/lodash
[ts-action]: https://www.npmjs.com/package/ts-action