finished going over the tutorial

This commit is contained in:
Yanick Champoux 2021-10-13 19:31:37 -04:00
parent 6dd8b1af9e
commit d75da07d3f
5 changed files with 182 additions and 228 deletions

View File

@ -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
```

View File

@ -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();
dispatch.incNextId()
next(action);
dispatch.addTodoWithId( action.payload, id );
}
}
});
todos.addEffect( add_todo, ({ getState, dispatch }) => next => action => {
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;
```
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

View File

@ -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,

View File

@ -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
View File

@ -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);
}
/**