documentation, actions

This commit is contained in:
Yanick Champoux 2021-10-12 18:13:59 -04:00
parent 2e357b71e2
commit 9778e04a8d
13 changed files with 151 additions and 140 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -89,4 +89,4 @@ well with <a href="https://immerjs.github.io/immer/docs/introduction">Immer</a>.
can be used to wrap all mutations with it:</p>
<pre><code><span class="hl-0">import</span><span class="hl-1"> </span><span class="hl-2">Updux</span><span class="hl-1"> </span><span class="hl-0">from</span><span class="hl-1"> </span><span class="hl-3">&#39;updux&#39;</span><span class="hl-1">;</span><br/><span class="hl-0">import</span><span class="hl-1"> { </span><span class="hl-2">produce</span><span class="hl-1"> } </span><span class="hl-0">from</span><span class="hl-1"> </span><span class="hl-3">&#39;Immer&#39;</span><span class="hl-1">;</span><br/><br/><span class="hl-4">const</span><span class="hl-1"> </span><span class="hl-5">updux</span><span class="hl-1"> = </span><span class="hl-4">new</span><span class="hl-1"> </span><span class="hl-6">Updux</span><span class="hl-1">({</span><br/><span class="hl-1"> </span><span class="hl-2">initial:</span><span class="hl-1"> { </span><span class="hl-2">counter:</span><span class="hl-1"> </span><span class="hl-7">0</span><span class="hl-1"> },</span><br/><span class="hl-1"> </span><span class="hl-6">groomMutations</span><span class="hl-2">:</span><span class="hl-1"> </span><span class="hl-2">mutation</span><span class="hl-1"> </span><span class="hl-4">=&gt;</span><span class="hl-1"> (...</span><span class="hl-2">args</span><span class="hl-1">) </span><span class="hl-4">=&gt;</span><span class="hl-1"> </span><span class="hl-6">produce</span><span class="hl-1">( </span><span class="hl-6">mutation</span><span class="hl-1">(...</span><span class="hl-2">args</span><span class="hl-1">) ),</span><br/><span class="hl-1"> </span><span class="hl-2">mutations:</span><span class="hl-1"> {</span><br/><span class="hl-1"> </span><span class="hl-6">add</span><span class="hl-2">:</span><span class="hl-1"> (</span><span class="hl-2">inc</span><span class="hl-1">=</span><span class="hl-7">1</span><span class="hl-1">) </span><span class="hl-4">=&gt;</span><span class="hl-1"> </span><span class="hl-2">draft</span><span class="hl-1"> </span><span class="hl-4">=&gt;</span><span class="hl-1"> </span><span class="hl-2">draft</span><span class="hl-1">.</span><span class="hl-2">counter</span><span class="hl-1"> += </span><span class="hl-2">inc</span><br/><span class="hl-1"> }</span><br/><span class="hl-1">});</span><br/>
</code></pre>
</div></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class="current"><a href="modules.html">Exports</a></li><li class=" tsd-kind-namespace"><a href="modules/_updux_.html">&quot;updux&quot;</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-type-alias tsd-has-type-parameter"><a href="modules.html#Dict" class="tsd-kind-icon">Dict</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="assets/main.js"></script></body></html>
</div></div><div class="col-4 col-menu menu-sticky-wrap menu-highlight"><nav class="tsd-navigation primary"><ul><li class="current"><a href="modules.html">Exports</a></li><li class=" tsd-kind-namespace"><a href="modules/_updux_.html">&quot;updux&quot;</a></li></ul></nav><nav class="tsd-navigation secondary menu-sticky"><ul><li class="tsd-kind-type-alias"><a href="modules.html#ActionGenerator" class="tsd-kind-icon">Action<wbr/>Generator</a></li><li class="tsd-kind-type-alias tsd-has-type-parameter"><a href="modules.html#Dict" class="tsd-kind-icon">Dict</a></li><li class="tsd-kind-type-alias tsd-has-type-parameter"><a href="modules.html#Mutation" class="tsd-kind-icon">Mutation</a></li></ul></nav></div></div></div><footer class="with-border-bottom"><div class="container"><h2>Settings</h2><p>Theme <select id="theme"><option value="os">OS</option><option value="light">Light</option><option value="dark">Dark</option></select></p></div></footer><div class="container tsd-generator"><p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p></div><div class="overlay"></div><script src="assets/main.js"></script></body></html>

File diff suppressed because one or more lines are too long

View File

@ -1,23 +1,21 @@
# What's Updux?
## What's Updux?
So, I'm a fan of [Redux](https://redux.js.org).
As I was looking into tools to help cut on its boilerplate,
I came across [rematch](https://rematch.github.io/rematch).
It has some pretty darn good ideas.
Keeping mutations and asynchronous effects close to the
reducer definition? Nice. Automatically infering the
actions from the said mutations and effects? Genius!
But it also enforces a flat hierarchy of reducers -- where
is the fun in that? And I'm also having a strong love for
[Updeep](https://github.com/substantial/updeep), so I want reducer state updates to leverage the heck out of it.
is the fun in that?
I'm also having a strong love for
[Updeep](https://github.com/substantial/updeep), so I wanted a framework where
I could use it to define reducer state updates.
Hence: `Updux`. Heavily inspired by `rematch`, but twisted
to work with `updeep` and to fit my peculiar needs. It offers features such as
* Mimic the way VueX has mutations (reducer reactions to specific actions) and
* Mimic the way VueX has mutations (per-action reducer logic) and
effects (middleware reacting to actions that can be asynchronous and/or
have side-effects), so all things pertaining to a store are defined
in the space place.
@ -26,9 +24,7 @@ to work with `updeep` and to fit my peculiar needs. It offers features such as
store.
* Mutations have a signature that is friendly to Updux and Immer.
* Mutations auto-unwrapping the payload of actions for you.
* TypeScript types.
* Leverage [ts-action](https://www.npmjs.com/package/ts-action) for action
creation.
* TypeScript support.
**Fair warning**: this package is still very new, likely to go through
big changes before I find the perfect balance between ease of use and sanity.
@ -36,70 +32,36 @@ Caveat Emptor.
# Synopsis
```
import Updux from 'updux';
import { action, payload } from 'ts-action';
```js
import { Updux } from 'updux';
import u from 'updeep';
import add from 'lodash/fp/add.js';
import otherDux from './otherUpdux';
const inc = action( 'INC', payload<int>() );
const updux = new Updux({
const dux = new Updux({
initial: {
counter: 0,
},
actions: {
inc
inc: null
},
subduxes: {
otherDux,
}
});
updux.addMutation( inc, increment => u({counter: s => s + increment }));
dux.setMutation('inc', (increment) => u({ counter: add(increment) }));
updux.addEffect( '*', api => next => action => {
dux.addEffect( '*', api => next => action => {
console.log( "hey, look, an action zoomed by!", action );
next(action);
} );
const myDux = updux.asDux;
const store = dux.createStore();
const store = myDux.createStore();
store.dispatch.inc(1);
store.dispatch( myDux.actions.inc(3) );
console.log( store.getState().counter ); // prints 1
```
# Description
The formal documentation of the class Updux and its associated functions and
types can be found over [here](https://yanick.github.io/updux/docs/classes/updux.html).
## Exporting upduxes
If you are creating upduxes that will be used as subduxes
by other upduxes, or as
[ducks](https://github.com/erikras/ducks-modular-redux)-like containers, I
recommend that you export the "compiled" (as in, no more editable and with all its properties resolved) output of the Updux instance via its `asDux()` getter:
```
import Updux from 'updux';
const updux = new Updux({ ... });
export default updux.asDux;
```
Then you can use them as subduxes like this:
```
import Updux from 'updux';
import foo from './foo'; // foo is a dux
import bar from './bar'; // bar is a dux as well
const updux = new Updux({
subduxes: {
foo, bar
}
});
```

View File

@ -1,4 +1,4 @@
- [README](/README.md)
- [Intro](/README.md)
- [Tutorial](/tutorial.md)
- [Concepts](/concepts.md)
- [Recipes](/recipes.md)

View File

@ -3,72 +3,45 @@
This tutorial walks you through the features of `Updux` using the
time-honored example of the implementation of Todo list store.
This tutorial assumes that our project is written in TypeScript, and
that we are using [updeep](https://www.npmjs.com/package/updeep) to
help with immutability and deep merging and [ts-action][] to manage our
actions. This is the recommended setup, but
neither of those two architecture
decisions are mandatory; Updux is equally usable in a pure-JavaScript setting,
and `updeep` can easily be substitued with, say, [immer][], [lodash][], or even
just plain JavaScript. Eventually, I plan to write a version of this tutorial
with all those different configurations.
Also, the code used here is also available in the project repository, in the
`src/tutorial` directory.
We'll be using
[updeep](https://www.npmjs.com/package/updeep) to
help with immutability and deep merging,
but that's totally optional. If `updeep` is not your bag,
it can easily be substitued with, say, [immer][], [lodash][], or even
plain JavaScript.
## Definition of the state
First thing first: let's define the type of our store:
To begin with, let's define that has nothing but an initial state.
```
type Todo = {
id: number;
description: string;
done: boolean;
};
```js
import { Updux } from 'updux';
type TodoStore = {
next_id: number;
todos: Todo[];
};
```
With that, let's create our very first Updux:
```
import Updux from 'updux';
const todosUpdux = new Updux({
const todosDux = new Updux({
initial: {
next_id: 1,
todos: [],
} as TodoStore
}
});
```
Note that we explicitly cast the initial state as `as TodoStore`. This lets
Updux know what is the store's state.
This being said, congrats! You have written your first Updux object. It
Congrats! You have written your first Updux object. It
doesn't do a lot, but you can already create a store out of it, and its
initial state will be automatically set:
```
const store = todosUpdux.createStore();
```js
const store = todosDux.createStore();
console.log(store.getState());
// { next_id: 1, todos: [] }
console.log(store.getState()); // prints { next_id: 1, todos: [] }
```
## Add actions
This is all good, but a little static. Let's add actions!
```
import { action, payload } from 'ts-action';
const add_todo = action('add_todo', payload<string>() );
const todo_done = action('todo_done', payload<number>() );
```js
todosDux.setAction( 'addTodo' );
todosDux.setAction( 'todoDone' );
```
Now, there is a lot of ways to add actions to a Updux object.

View File

@ -128,7 +128,7 @@ export class Updux {
this.#subscriptions = [...this.#subscriptions, subscription];
}
addAction(type, payloadFunc) {
setAction(type, payloadFunc) {
const theAction = action(type, payloadFunc);
this.#actions = { ...this.#actions, [type]: theAction };
@ -144,7 +144,7 @@ export class Updux {
return func;
}
addMutation(name, mutation) {
setMutation(name, mutation) {
if (typeof name === 'function') name = name.type;
this.#mutations = {
@ -152,7 +152,7 @@ export class Updux {
[name]: mutation,
};
return this;
return mutation;
}
addEffect(action, effect) {
@ -304,6 +304,3 @@ export class Updux {
return store;
}
}
const x = new Updux();
x.selectors;

View File

@ -45,13 +45,13 @@ test('basic actions', async (t) => {
t.same(Object.keys(dux.actions).sort(), ['bar', 'foo']);
});
test('addAction', async (t) => {
test('setAction', async (t) => {
const dux = new Updux({
actions: {
bar: action('bar'),
},
});
dux.addAction('foo');
dux.setAction('foo');
t.same(Object.keys(dux.actions).sort(), ['bar', 'foo']);
});
@ -81,7 +81,7 @@ test('basic selectors', async (t) => {
(add) =>
add + foo
);
dux.addAction('stuff');
dux.setAction('stuff');
t.equal(dux.selectors.getBar({ bar: 3 }), 3);
t.equal(dux.selectors.getFoo({ foo: 3 }), 3);
@ -100,8 +100,8 @@ test('mutations', async (t) => {
const alpha = new Updux({
initial: { quux: 3 },
});
alpha.addAction('add');
alpha.addMutation('add', (toAdd) => (state) => ({
alpha.setAction('add');
alpha.setMutation('add', (toAdd) => (state) => ({
quux: state.quux + toAdd,
}));
@ -112,12 +112,12 @@ test('mutations', async (t) => {
},
subduxes: { alpha },
});
dux.addAction('subtract');
dux.addMutation('add', (toAdd) => (state) => ({
dux.setAction('subtract');
dux.setMutation('add', (toAdd) => (state) => ({
...state,
foo: state.foo + toAdd,
}));
dux.addMutation('subtract', (toSubtract) => (state) => ({
dux.setMutation('subtract', (toSubtract) => (state) => ({
...state,
bar: state.bar - toSubtract,
}));

48
src/documentation.test.js Normal file
View File

@ -0,0 +1,48 @@
import { test } from 'tap';
import u from 'updeep';
import add from 'lodash/fp/add.js';
import { Updux } from './index.js';
test('README.md', async (t) => {
const otherDux = new Updux({});
const dux = new Updux({
initial: {
counter: 0,
},
actions: {
inc: null,
},
subduxes: {
otherDux,
},
});
dux.setMutation('inc', (increment) => u({ counter: add(increment) }));
dux.addEffect('*', (api) => (next) => (action) => {
next(action);
});
const store = dux.createStore();
store.dispatch.inc(1);
t.equal(store.getState().counter, 1);
});
test('tutorial', async (t) => {
const todosDux = new Updux({
initial: {
next_id: 1,
todos: [],
}
});
todosDux.setAction( 'addTodo' );
todosDux.setAction( 'todoDone' );
})

View File

@ -1,16 +1,2 @@
import Updux from './Updux.js';
export { Updux } from './Updux.js';
export {
/**
* The {@link Updux} class
*/
Updux
};
/**
* `Updux` is a way to minimize and simplify the boilerplate associated with the
* creation of a `Redux` store. It takes a shorthand configuration
* object, and generates the appropriate reducer, actions, middleware, etc.
* In true `Redux`-like fashion, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state.
* @module updux
*/

View File

@ -81,7 +81,7 @@ test('order of processing', async (t) => {
t.same(dux.reducer(undefined, foo()), { x: ['subdux', 'main'] });
});
test('addMutation', async (t) => {
test('setMutation', async (t) => {
const foo = action('foo');
const dux = new Updux({
@ -90,13 +90,13 @@ test('addMutation', async (t) => {
t.equal(dux.reducer(undefined, foo()), '', 'noop');
dux.addMutation('foo', () => () => 'foo');
dux.setMutation('foo', () => () => 'foo');
t.equal(dux.reducer(undefined, foo()), 'foo', 'foo was added');
await t.test('name as function', async (t) => {
const bar = action('bar');
dux.addMutation(bar, () => () => 'bar');
dux.setMutation(bar, () => () => 'bar');
t.equal(dux.reducer(undefined, bar()), 'bar', 'bar was added');
});

37
types/index.d.ts vendored
View File

@ -1,5 +1,15 @@
type Dict<T> = Record<string, T>;
type Mutation<TState = unknown> = (
payload: unknown,
action: Record<string, unknown>
) => (state: TState) => TState;
type ActionGenerator = { type: string } & ((...args: any[]) => {
type: string;
payload?: unknown;
});
declare module 'updux' {
/**
* Configuration object typically passed to the constructor of the class Updux.
@ -14,7 +24,7 @@ declare module 'updux' {
/**
* Subduxes to be merged to this dux.
*/
subduxes?: Dict<Updux|UpduxConfig>;
subduxes?: Dict<Updux | UpduxConfig>;
/**
* Local actions.
@ -59,5 +69,30 @@ declare module 'updux' {
get initial(): TState;
get selectors(): unknown;
/**
* Sets the local mutation for the given action.
*
* The action can be specified via its type
* or its generator.
*
* Returns the same mutation function.
*/
setMutation<TMutation extends Mutation>(
actionType: string,
mutation: TMutation
): TMutation;
setMutation<TMutation extends Mutation>(
action: ActionGenerator,
mutation: TMutation
): TMutation;
/**
* Registers the action for the dux.
* If no payload function is provided, whatever is
* given as an argument to the action generator will
* be set as-is in the action's payload.
*/
setAction(actionType: string, payloadFunc?: Function);
}
}