terminal mutations

This commit is contained in:
Yanick Champoux 2022-08-30 16:10:38 -04:00
parent 82e8ba7385
commit 13c9603251
5 changed files with 162 additions and 2 deletions

View File

@ -1,2 +1,3 @@
* [Home](/) * [Home](/)
* [ Tutorial ](tutorial.md) * [ Tutorial ](tutorial.md)
* [ Recipes ](recipes.md)

96
docs/recipes.md Normal file
View File

@ -0,0 +1,96 @@
# Recipes
## Mapping a mutation to all values of a state
Say you have a `todos` state that is an array of `todo` sub-states, with some
actions that should percolate to all todos, and some that should only
percolate to one. One way to model this is via updux's splat subduxes
(backed by `updeep`'s own '*'-key behavior).
```
const done = () => (state) => ({...state, done: true});
const todo = new Updux({
initial: { id: 0, done: false },
actions: {
done: null,
doneAll: null,
},
mutations: {
done,
doneAll: done,
},
});
const todos = new Updux({
initial: [],
subduxes: { '*': todo },
actions: { addTodo: null },
mutations: {
addTodo: text => state => [ ...state, { text } ]
}
});
todos.setMutation(
todo.actions.done,
(text,action) => u.map(u.if(u.is('text',text), todo.upreducer(action))),
true // prevents the subduxes mutations to run automatically
);
```
## Usage with Immer
While Updux was created with Updeep in mind, it also plays very
well with [Immer](https://immerjs.github.io/immer/docs/introduction).
For example, taking this basic updux:
```
import Updux from 'updux';
const updux = new Updux({
initial: { counter: 0 },
mutations: {
add: (inc=1) => state => ({ counter: state.counter + inc })
}
});
```
Converting it to Immer would look like:
```
import Updux from 'updux';
import { produce } from 'immer';
const updux = new Updux({
initial: { counter: 0 },
mutations: {
add: (inc=1) => produce( draft => draft.counter += inc ) }
}
});
```
But since typing `produce` over and over is no fun, `groomMutations`
can be used to wrap all mutations with it:
```
import Updux from 'updux';
import { produce } from 'immer';
const updux = new Updux({
initial: { counter: 0 },
groomMutations: mutation => (...args) => produce( mutation(...args) ),
mutations: {
add: (inc=1) => draft => draft.counter += inc
}
});
```

54
docs/recipes.test.js Normal file
View File

@ -0,0 +1,54 @@
import { test, expect } from 'vitest';
import u from 'updeep';
import { Updux } from '../src/index.js';
const done = () => (state) => ({...state, done: true});
const todo = new Updux({
initial: { id: 0, done: false },
actions: {
done: null,
doneAll: null,
},
mutations: {
done,
doneAll: done,
},
});
const todos = new Updux({
initial: [],
subduxes: { '*': todo },
actions: { addTodo: null },
mutations: {
addTodo: text => state => [ ...state, { text } ]
}
});
todos.setMutation(
todo.actions.done,
(text,action) => u.map(u.if(u.is('text',text), todo.upreducer(action))),
true // prevents the subduxes mutations to run automatically
);
test( "tutorial", async () => {
const store = todos.createStore();
store.dispatch.addTodo('one');
store.dispatch.addTodo('two');
store.dispatch.addTodo('three');
store.dispatch.done( 'two' );
expect( store.getState()[1].done ).toBeTruthy();
expect( store.getState()[2].done ).toBeFalsy();
store.dispatch.doneAll();
expect( store.getState().map( ({done}) => done ) ).toEqual([
true, true, true
]);
});

View File

@ -107,9 +107,11 @@ export class Updux {
* updux if not already present (the idea being that making a typo on a string * updux if not already present (the idea being that making a typo on a string
* is easy, but passing a wrong function very more unlikely). * is easy, but passing a wrong function very more unlikely).
* @param {Function} mutation - Mutating function. * @param {Function} mutation - Mutating function.
* @param {bool} terminal - If true, subduxes' mutations won't be invoked on
* the action.
* @return {void} * @return {void}
*/ */
setMutation(action, mutation) { setMutation(action, mutation, terminal = false) {
// TODO option strict: false to make it okay to auto-create // TODO option strict: false to make it okay to auto-create
// the actions as strings? // the actions as strings?
if (action.type) { if (action.type) {
@ -128,6 +130,12 @@ export class Updux {
throw new Error(`action '${action}' is not defined`); throw new Error(`action '${action}' is not defined`);
} }
if( terminal ) {
const originalMutation = mutation;
mutation = (...args) => originalMutation(...args);
mutation.terminal = true;
}
this.#mutations[action] = mutation; this.#mutations[action] = mutation;
} }

View File

@ -28,6 +28,7 @@ const subMutations = (subduxes) => (action) => (state) => {
export function buildUpreducer(mutations, subduxes) { export function buildUpreducer(mutations, subduxes) {
return (action) => (state) => { return (action) => (state) => {
if( ! mutations[action.type]?.terminal )
state = subMutations(subduxes)(action)(state); state = subMutations(subduxes)(action)(state);
return localMutation(mutations)(action)(state); return localMutation(mutations)(action)(state);