From d75da07d3f8be9f439c5e2e351cd7e7139794f0e Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Wed, 13 Oct 2021 19:31:37 -0400 Subject: [PATCH] finished going over the tutorial --- docs/concepts.md | 34 +++++ docs/tutorial.md | 364 ++++++++++++++++++---------------------------- src/Updux.js | 3 +- src/Updux.test.js | 4 +- types/index.d.ts | 5 + 5 files changed, 182 insertions(+), 228 deletions(-) diff --git a/docs/concepts.md b/docs/concepts.md index e71f6a5..4579601 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -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 + +``` diff --git a/docs/tutorial.md b/docs/tutorial.md index 67dd65d..4cfedee 100644 --- a/docs/tutorial.md +++ b/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() ); -const add_todo_with_id = action('add_todo_with_id', - payload<{ description: string; id: number }>() ); -const todo_done = action('todo_done', payload() ); -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() ); - -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; - -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() ); - -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 diff --git a/src/Updux.js b/src/Updux.js index 07bb112..2e18570 100644 --- a/src/Updux.js +++ b/src/Updux.js @@ -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, diff --git a/src/Updux.test.js b/src/Updux.test.js index 2f66b33..7499803 100644 --- a/src/Updux.test.js +++ b/src/Updux.test.js @@ -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) => diff --git a/types/index.d.ts b/types/index.d.ts index 9ac6d51..d939f1e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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); } /**