From 13c96032514cb2c760bfd0b1d3a88cd9955af799 Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Tue, 30 Aug 2022 16:10:38 -0400 Subject: [PATCH] terminal mutations --- docs/_sidebar.md | 1 + docs/recipes.md | 96 ++++++++++++++++++++++++++++++++++++++++++++ docs/recipes.test.js | 54 +++++++++++++++++++++++++ src/Updux.js | 10 ++++- src/upreducer.js | 3 +- 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 docs/recipes.md create mode 100644 docs/recipes.test.js diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 7d00bc8..f156e0d 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -1,2 +1,3 @@ * [Home](/) * [ Tutorial ](tutorial.md) +* [ Recipes ](recipes.md) diff --git a/docs/recipes.md b/docs/recipes.md new file mode 100644 index 0000000..3dd308d --- /dev/null +++ b/docs/recipes.md @@ -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 + } +}); + +``` + + + diff --git a/docs/recipes.test.js b/docs/recipes.test.js new file mode 100644 index 0000000..8560675 --- /dev/null +++ b/docs/recipes.test.js @@ -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 + ]); + +}); + diff --git a/src/Updux.js b/src/Updux.js index b32af1d..dd58c72 100644 --- a/src/Updux.js +++ b/src/Updux.js @@ -107,9 +107,11 @@ export class Updux { * 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). * @param {Function} mutation - Mutating function. + * @param {bool} terminal - If true, subduxes' mutations won't be invoked on + * the action. * @return {void} */ - setMutation(action, mutation) { + setMutation(action, mutation, terminal = false) { // TODO option strict: false to make it okay to auto-create // the actions as strings? if (action.type) { @@ -128,6 +130,12 @@ export class Updux { throw new Error(`action '${action}' is not defined`); } + if( terminal ) { + const originalMutation = mutation; + mutation = (...args) => originalMutation(...args); + mutation.terminal = true; + } + this.#mutations[action] = mutation; } diff --git a/src/upreducer.js b/src/upreducer.js index fc09070..85ac320 100644 --- a/src/upreducer.js +++ b/src/upreducer.js @@ -28,7 +28,8 @@ const subMutations = (subduxes) => (action) => (state) => { export function buildUpreducer(mutations, subduxes) { return (action) => (state) => { - state = subMutations(subduxes)(action)(state); + if( ! mutations[action.type]?.terminal ) + state = subMutations(subduxes)(action)(state); return localMutation(mutations)(action)(state); };