finished going over the tutorial
This commit is contained in:
parent
6dd8b1af9e
commit
d75da07d3f
@ -75,3 +75,37 @@ An effect has the signature
|
||||
```js
|
||||
const effect = ({ getState, dispatch }) => next => action => { ... }
|
||||
```
|
||||
|
||||
|
||||
## Selectors
|
||||
|
||||
Selectors are used to get data derived from the store's state.
|
||||
|
||||
The signature of a selector function is either `(state) => result`
|
||||
or `(state) => (...args) => result`. The selector instances attached
|
||||
to a store's `getState` already do the first call on `state`, so you don't have to.
|
||||
|
||||
```js
|
||||
|
||||
const dux = new Updux({
|
||||
initial: {
|
||||
foo: 1,
|
||||
},
|
||||
selectors: {
|
||||
getFoo: ({foo}) => foo,
|
||||
getFooPlus: ({foo}) => increment => foo + increment,
|
||||
}
|
||||
});
|
||||
|
||||
console.log(dux.selectors.getFoo({foo: 2})); // prints 2
|
||||
console.log(dux.selectors.getFooPlus({foo: 2})(3)); // prints 5
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
console.log(store.selectors.getFoo({foo: 2})); // prints 2
|
||||
console.log(store.selectors.getFooPlus({foo: 2})(3)); // prints 5
|
||||
|
||||
console.log(store.getState.getFoo()); // prints 2
|
||||
console.log(store.getState.getFooPlus(3)); // prints 5
|
||||
|
||||
```
|
||||
|
358
docs/tutorial.md
358
docs/tutorial.md
@ -85,23 +85,29 @@ todosDux.setMutation( 'addTodo', description => ({next_id: id, todos}) => ({
|
||||
In addition to mutations, Updux also provides action-specific middleware, here
|
||||
called effects.
|
||||
|
||||
Effects use the usual Redux middleware signature:
|
||||
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.
|
||||
|
||||
```
|
||||
```js
|
||||
import u from 'updeep';
|
||||
|
||||
// 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 'add_todo'.
|
||||
// a new todo. So let's use a new version of the action 'addTodo'.
|
||||
|
||||
const add_todo_with_id = action('add_todo_with_id', payload<{description: string; id?: number}>() );
|
||||
const inc_next_id = action('inc_next_id');
|
||||
const addTodoWithId = action('addTodoWithId');
|
||||
const incNextId = action('incNextId');
|
||||
const addTodo = action('addTodo');
|
||||
|
||||
const populate_next_id = ({ getState, dispatch }) => next => action => {
|
||||
const { next_id: id } = getState();
|
||||
const addTodoEffect = ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.nextId();
|
||||
|
||||
dispatch.incNextId();
|
||||
|
||||
dispatch(inc_next_id());
|
||||
next(action);
|
||||
dispatch( add_todo_with_id({ description: action.payload, id }) );
|
||||
|
||||
dispatch.addTodoWithId({ description: action.payload, id }) );
|
||||
}
|
||||
```
|
||||
|
||||
@ -109,28 +115,33 @@ 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 ]
|
||||
]
|
||||
})
|
||||
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');
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
const todosUpdux = new Updux({
|
||||
actions: { add_todo, inc_next_id },
|
||||
});
|
||||
|
||||
todosUpdux.addEffect( add_todo, populate_next_id );
|
||||
todosDux.addEffect( 'addTodo', addTodoEffect );
|
||||
```
|
||||
|
||||
As for the mutations, for TypeScript projects
|
||||
the use of `addEffect` is prefered, as the method gives visibility to the
|
||||
action and state types.
|
||||
|
||||
### Catch-all effect
|
||||
|
||||
It is possible to have an effect match all actions via the special `*` token.
|
||||
@ -142,14 +153,11 @@ todosUpdux.addEffect('*', () => next => action => {
|
||||
});
|
||||
```
|
||||
|
||||
## Selectors
|
||||
## Adding 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`.
|
||||
time or via `setSelector`.
|
||||
|
||||
```
|
||||
import fp from 'lodash/fp';
|
||||
@ -166,287 +174,210 @@ const todosUpdux = new Updux({
|
||||
or
|
||||
|
||||
```
|
||||
todosUpdux.addSelector('getTodoById', ({todos}) => id => fp.find({id},todos));
|
||||
todosDux.setSelector('getTodoById', ({todos}) => id => fp.find({id},todos));
|
||||
```
|
||||
|
||||
Here the declaration as part of the constructor configuration is prefered.
|
||||
Whereas the `addSelector` will provide the state's type as part of its
|
||||
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`.
|
||||
The `getState` method of a dux store is augmented
|
||||
with its selectors, with the first call for the state already
|
||||
called in for you.
|
||||
|
||||
```
|
||||
const store = todosUpdux.createStore();
|
||||
```js
|
||||
const store = todosDux.createStore();
|
||||
|
||||
console.log(
|
||||
todosUpdux.selectors.getTodoById( store.getState() )(1)
|
||||
todosUpdux.getState.getTodoById(1)
|
||||
);
|
||||
```
|
||||
|
||||
## Subduxes
|
||||
|
||||
Now that we have all the building blocks, we can embark on the last, and best,
|
||||
part of Updux: its recursive nature.
|
||||
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 updux, undivided
|
||||
### 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 Updux we have so far:
|
||||
let's recap on the Todos dux we have so far:
|
||||
|
||||
```
|
||||
```js
|
||||
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>() );
|
||||
const add_todo_with_id = action('add_todo_with_id',
|
||||
payload<{ description: string; id: number }>() );
|
||||
const todo_done = action('todo_done', payload<number>() );
|
||||
const increment_next_id = action('increment_next_id');
|
||||
|
||||
const todosUpdux = new Updux({
|
||||
const todosDux = new Updux({
|
||||
initial: {
|
||||
next_id: 1,
|
||||
nextId: 1,
|
||||
todos: [],
|
||||
} as TodoStore,
|
||||
},
|
||||
actions: {
|
||||
add_todo,
|
||||
add_todo_with_id,
|
||||
todo_done,
|
||||
increment_next_id,
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
todosUpdux.addMutation( add_todo_with_id, payload =>
|
||||
u.updateIn( 'todos', todos => [ ...todos, { ...payload, done: false }] )
|
||||
);
|
||||
|
||||
todosUpdux.addMutation( increment_next_id, () => u({ next_id: i => i + 1 }) );
|
||||
|
||||
todosUpdux.addMutation( todo_done, id => u.updateIn(
|
||||
'todos', u.map( u.if( fp.matches({id}), todo => u({done: true}, todo) ) )
|
||||
) );
|
||||
|
||||
todosUpdux.addEffect( add_todo, ({ getState, dispatch }) => next => action => {
|
||||
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
|
||||
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.
|
||||
|
||||
### Next_id updux
|
||||
### NextId dux
|
||||
|
||||
```
|
||||
// dux/next_id.ts
|
||||
// dux/nextId.js
|
||||
|
||||
import Updux from 'updux';
|
||||
import { action, payload } from 'ts-action';
|
||||
import { Updux } from 'updux';
|
||||
import u from 'updeep';
|
||||
import fp from 'lodash/fp';
|
||||
|
||||
const increment_next_id = action('increment_next_id');
|
||||
|
||||
const updux = new Updux({
|
||||
export default new Updux({
|
||||
initial: 1,
|
||||
actions: {
|
||||
increment_next_id,
|
||||
incNextId: null,
|
||||
},
|
||||
selectors: {
|
||||
getNextId: state => state
|
||||
},
|
||||
mutations: {
|
||||
incrementNextId: () => fp.add(1)
|
||||
}
|
||||
});
|
||||
|
||||
updux.addMutation( increment_next_id, () => fp.add(1) );
|
||||
|
||||
export default updux.asDux;
|
||||
|
||||
```
|
||||
|
||||
Notice that we didn't have to specify the type of `initial`;
|
||||
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 { Updux } from 'updux';
|
||||
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({
|
||||
export default new Updux({
|
||||
initial: {
|
||||
next_id: 0,
|
||||
id: 0,
|
||||
description: "",
|
||||
done: false,
|
||||
} as Todo,
|
||||
},
|
||||
actions: {
|
||||
todo_done
|
||||
todoDone: null,
|
||||
},
|
||||
mutations: {
|
||||
todoDone: id => u.if( fp.matches({id}), { done: true }) )
|
||||
}
|
||||
});
|
||||
|
||||
updux.addMutation( todo_done, id => u.if( fp.matches({id}), { done: true }) );
|
||||
|
||||
export default updux.asDux;
|
||||
|
||||
```
|
||||
|
||||
### Todos updux
|
||||
|
||||
```
|
||||
// dux/todos/index.ts
|
||||
// dux/todos/index.js
|
||||
|
||||
import Updux, { DuxState } from 'updux';
|
||||
import { action, payload } from 'ts-action';
|
||||
import { Updux } from 'updux';
|
||||
import u from 'updeep';
|
||||
import fp from 'lodash/fp';
|
||||
|
||||
import todo from './todo';
|
||||
import todo from './todo/index.js';
|
||||
|
||||
type TodoState = DuxState<typeof todo>;
|
||||
|
||||
const add_todo_with_id = action('add_todo_with_id',
|
||||
payload<{ description: string; id: number }>()
|
||||
);
|
||||
|
||||
const updux = new Updux({
|
||||
initial: [] as Todo[],
|
||||
export default new Updux({
|
||||
initial: [],
|
||||
subduxes: {
|
||||
'*': todo.upreducer
|
||||
'*': todo
|
||||
},
|
||||
actions: {
|
||||
add_todo_with_id,
|
||||
addTodoWithId,
|
||||
},
|
||||
selectors: {
|
||||
mappedSelectors: {
|
||||
getTodoById: state => id => fp.find({id},state)
|
||||
},
|
||||
mutations: {
|
||||
addTodoWithId: todo =>
|
||||
todos => [ ...todos, todo ]
|
||||
}
|
||||
});
|
||||
|
||||
todosUpdux.addMutation( add_todo_with_id, payload =>
|
||||
todos => [ ...todos, { ...payload, done: false }]
|
||||
);
|
||||
|
||||
export default updux.asDux;
|
||||
```
|
||||
|
||||
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.
|
||||
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
|
||||
a reducer for the dux using the signature `(payload,action) => state => new_state`) and `reducer` in the second case (which yield an equivalent
|
||||
reducer using the classic signature `(state,action) => new_state`).
|
||||
|
||||
### Main store
|
||||
|
||||
```
|
||||
// dux/index.ts
|
||||
// dux/index.js
|
||||
|
||||
import Updux from 'updux';
|
||||
|
||||
import todos from './todos';
|
||||
import next_id from './next_id';
|
||||
import nextId from './next_id';
|
||||
|
||||
const add_todo = action('add_todo', payload<string>() );
|
||||
|
||||
const updux = new Updux({
|
||||
export new Updux({
|
||||
subduxes: {
|
||||
next_id,
|
||||
nextId,
|
||||
todos,
|
||||
},
|
||||
actions: {
|
||||
add_todo
|
||||
}
|
||||
});
|
||||
addTodo: null
|
||||
},
|
||||
effects: {
|
||||
addTodo: ({ getState, dispatch }) => next => action => {
|
||||
const id = getState.getNextId();
|
||||
|
||||
todos.addEffect( add_todo, ({ getState, dispatch }) => next => action => {
|
||||
const id = updux.selectors.getNextId( getState() );
|
||||
|
||||
dispatch(updux.actions.inc_next_id());
|
||||
dispatch.incNextId()
|
||||
|
||||
next(action);
|
||||
|
||||
dispatch( updux.actions.add_todo_with_id({ description: action.payload, id }) );
|
||||
dispatch.addTodoWithId( action.payload, id );
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default updux.asDux;
|
||||
|
||||
```
|
||||
|
||||
Tadah! We had to define the `add_todo` effect at the top level as it needs to
|
||||
access the `getNextId` selector from `next_id` and the `add_todo_with_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. i.e., the `getNextId` selector
|
||||
at the main level is actually defined as:
|
||||
access the right slice of the top object. ```
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
const getNextId = state => next_id.selectors.getNextId(state.next_id);
|
||||
```
|
||||
|
||||
## 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) => {
|
||||
(storeApi) => (state, previousState, unsubscribe) => {
|
||||
...
|
||||
}
|
||||
```
|
||||
@ -462,46 +393,29 @@ local `state` changed since their last invocation.
|
||||
Example:
|
||||
|
||||
```
|
||||
const set_nbr_todos = action('set_nbr_todos', payload() );
|
||||
|
||||
const todos = dux({
|
||||
const todos = new Updux({
|
||||
initial: [],
|
||||
subscriptions: [
|
||||
({dispatch}) => todos => dispatch(set_nbr_todos(todos.length))
|
||||
actions: {
|
||||
setNbrTodos: null,
|
||||
}
|
||||
reactions: [
|
||||
({dispatch}) => todos => dispatch.setNbrTodos(todos.length);
|
||||
],
|
||||
});
|
||||
|
||||
const myDux = dux({
|
||||
const myDux = new Updux({
|
||||
initial: {
|
||||
nbr_todos: 0
|
||||
},
|
||||
subduxes: {
|
||||
todos,
|
||||
},
|
||||
mutations: [
|
||||
[ set_nbr_todos, nbr_todos => u({nbr_todos}) ]
|
||||
]
|
||||
mutations: {
|
||||
setNbrTodos: nbrTodos => u({ nbrTodos })
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## 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
|
||||
`asDux` has more precise typing, which in result results in better typing of
|
||||
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
|
||||
|
@ -136,7 +136,8 @@ export class Updux {
|
||||
return theAction;
|
||||
}
|
||||
|
||||
addSelector(name, func) {
|
||||
setSelector(name, func) {
|
||||
// TODO selector already exists? Complain!
|
||||
this.#selectors = {
|
||||
...this.#selectors,
|
||||
[name]: func,
|
||||
|
@ -74,8 +74,8 @@ test('basic selectors', async (t) => {
|
||||
getBar: ({ bar }) => bar,
|
||||
},
|
||||
});
|
||||
dux.addSelector('getFoo', ({ foo }) => foo);
|
||||
dux.addSelector(
|
||||
dux.setSelector('getFoo', ({ foo }) => foo);
|
||||
dux.setSelector(
|
||||
'getAdd',
|
||||
({ foo }) =>
|
||||
(add) =>
|
||||
|
5
types/index.d.ts
vendored
5
types/index.d.ts
vendored
@ -94,6 +94,11 @@ declare module 'updux' {
|
||||
* be set as-is in the action's payload.
|
||||
*/
|
||||
setAction(actionType: string, payloadFunc?: Function);
|
||||
|
||||
/**
|
||||
* Registers a selector for the dux.
|
||||
*/
|
||||
setSelector(name: string, selector: Selector);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user