Compare commits
No commits in common. "66c2b162db2903e81cc1cb034ee248394c66745b" and "7280381ca8286c99340fc0853c1e948db2d61b30" have entirely different histories.
66c2b162db
...
7280381ca8
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,5 +11,3 @@ GPUCache/
|
|||||||
updux-2.0.0.tgz
|
updux-2.0.0.tgz
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
updux-5.0.0.tgz
|
updux-5.0.0.tgz
|
||||||
.envrc
|
|
||||||
.task
|
|
||||||
|
11
TODO
11
TODO
@ -1,9 +1,2 @@
|
|||||||
* documentation action + mutation <<<
|
- documentation generator (mkdocs + jsdoc-to-markdown)
|
||||||
* createAction
|
- createStore
|
||||||
* addMutation
|
|
||||||
|
|
||||||
addMutation with signature
|
|
||||||
|
|
||||||
.addMutation( actionCreator, (payload,action) => state => newState )
|
|
||||||
|
|
||||||
* then check that the mutation is in the new type
|
|
||||||
|
@ -20,8 +20,7 @@ tasks:
|
|||||||
- git checkout {{.PARENT_BRANCH}}
|
- git checkout {{.PARENT_BRANCH}}
|
||||||
- git weld -
|
- git weld -
|
||||||
|
|
||||||
test: vitest run src
|
test: vitest run src docs/*.ts
|
||||||
|
|
||||||
test:dev: vitest src
|
test:dev: vitest src
|
||||||
|
|
||||||
lint:fix:delta:
|
lint:fix:delta:
|
||||||
@ -66,20 +65,6 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- npx eslint {{.FILES | default "src/**" }}
|
- npx eslint {{.FILES | default "src/**" }}
|
||||||
|
|
||||||
docs:api:
|
|
||||||
sources: [src/**.ts]
|
|
||||||
generates: [docs/src/content/docs/api/**.md]
|
|
||||||
cmds:
|
|
||||||
- typedoc
|
|
||||||
- contrib/api_grooming.pl docs/src/content/docs/api
|
|
||||||
docs:serve:
|
|
||||||
cmds:
|
|
||||||
- mkdocs serve
|
|
||||||
|
|
||||||
typedoc:
|
|
||||||
cmds:
|
|
||||||
- typedoc
|
|
||||||
|
|
||||||
docsify-install:
|
docsify-install:
|
||||||
cmds:
|
cmds:
|
||||||
- cp -r node_modules/docsify/lib docs/docsify
|
- cp -r node_modules/docsify/lib docs/docsify
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env perl
|
|
||||||
|
|
||||||
use 5.34.0;
|
|
||||||
|
|
||||||
use Path::Tiny;
|
|
||||||
|
|
||||||
my $dir = path(shift);
|
|
||||||
|
|
||||||
$dir->visit(sub{
|
|
||||||
my( $path ) = @_;
|
|
||||||
return unless $path =~ /\.md$/;
|
|
||||||
|
|
||||||
$path->edit(sub{
|
|
||||||
s/.*^\# (.*?)$/---\ntitle: "@{[ $1 =~ s(\\)()gr]}"\n---/sm;
|
|
||||||
});
|
|
||||||
},{ recurse => 1});
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
|||||||
# What's Updux?
|
|
||||||
|
|
||||||
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
|
|
||||||
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
|
|
||||||
|
|
||||||
It has a couple of pretty good ideas that removes some of the
|
|
||||||
boilerplate. 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.
|
|
||||||
|
|
||||||
All that to say, say hello to `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
|
|
||||||
effects (middleware reacting to actions that can be asynchronous and/or
|
|
||||||
have side-effects), so everything pertaining to a store are all defined
|
|
||||||
in the space place.
|
|
||||||
- Automatically gather all actions used by the updux's effects and mutations,
|
|
||||||
and makes then accessible as attributes to the `dispatch` object of the
|
|
||||||
store.
|
|
||||||
- Mutations have a signature that is friendly to Updux and Immer.
|
|
||||||
- Also, the mutation signature auto-unwrap the payload of the actions for you.
|
|
||||||
- TypeScript types.
|
|
||||||
|
|
||||||
Fair warning: this package is still very new, probably very buggy,
|
|
||||||
definitively very badly documented, and very subject to changes. Caveat
|
|
||||||
Maxima Emptor.
|
|
||||||
|
|
||||||
# Synopsis
|
|
||||||
|
|
||||||
```
|
|
||||||
import updux from 'updux';
|
|
||||||
|
|
||||||
import otherUpdux from './otherUpdux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
initial,
|
|
||||||
reducer,
|
|
||||||
actions,
|
|
||||||
middleware,
|
|
||||||
createStore,
|
|
||||||
} = new Updux({
|
|
||||||
initial: {
|
|
||||||
counter: 0,
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
otherUpdux,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
inc: ( increment = 1 ) => u({counter: s => s + increment })
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
'*' => api => next => action => {
|
|
||||||
console.log( "hey, look, an action zoomed by!", action );
|
|
||||||
next(action);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
customAction: ( someArg ) => ({
|
|
||||||
type: "custom",
|
|
||||||
payload: { someProp: someArg }
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = createStore();
|
|
||||||
|
|
||||||
store.dispatch.inc(3);
|
|
||||||
```
|
|
||||||
|
|
||||||
# Description
|
|
||||||
|
|
||||||
Full documentation can be [found here](https://yanick.github.io/updux/).
|
|
||||||
Right now the best way to understand the whole thing is to go
|
|
||||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
|
||||||
|
|
||||||
## 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 Updux instance as the default export:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
|
|
||||||
const updux = new Updux({ ... });
|
|
||||||
|
|
||||||
export default updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use them as subduxes like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
import foo from './foo'; // foo is an Updux
|
|
||||||
import bar from './bar'; // bar is an Updux as well
|
|
||||||
|
|
||||||
const updux = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
foo, bar
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if you want to use it:
|
|
||||||
|
|
||||||
```
|
|
||||||
import updux from './myUpdux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
reducer,
|
|
||||||
actions: { doTheThing },
|
|
||||||
createStore,
|
|
||||||
middleware,
|
|
||||||
} = updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mapping a mutation to all values of a state
|
|
||||||
|
|
||||||
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
|
||||||
enough to have the main reducer maps away all items to the sub-reducer:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({ initial: [] });
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.review,
|
|
||||||
(_,action) => state => state.map( todo.upreducer(action) )
|
|
||||||
);
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
But `updeep` can iterate through all the items of an array (or the values of
|
|
||||||
an object) via the special key `*`. So the todos updux above could also be
|
|
||||||
written:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({
|
|
||||||
subduxes: { '*': todo },
|
|
||||||
});
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The advantages being that the actions/mutations/effects of the subdux will be
|
|
||||||
imported by the root updux as usual, and all actions that aren't being
|
|
||||||
overridden by a sink mutation will trickle down 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: 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
@ -1 +0,0 @@
|
|||||||
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
|
|
@ -1,228 +0,0 @@
|
|||||||
updux / [Exports](modules.md)
|
|
||||||
|
|
||||||
# What's Updux?
|
|
||||||
|
|
||||||
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
|
|
||||||
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
|
|
||||||
|
|
||||||
It has a couple of pretty good ideas that removes some of the
|
|
||||||
boilerplate. 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.
|
|
||||||
|
|
||||||
All that to say, say hello to `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
|
|
||||||
effects (middleware reacting to actions that can be asynchronous and/or
|
|
||||||
have side-effects), so everything pertaining to a store are all defined
|
|
||||||
in the space place.
|
|
||||||
- Automatically gather all actions used by the updux's effects and mutations,
|
|
||||||
and makes then accessible as attributes to the `dispatch` object of the
|
|
||||||
store.
|
|
||||||
- Mutations have a signature that is friendly to Updux and Immer.
|
|
||||||
- Also, the mutation signature auto-unwrap the payload of the actions for you.
|
|
||||||
- TypeScript types.
|
|
||||||
|
|
||||||
Fair warning: this package is still very new, probably very buggy,
|
|
||||||
definitively very badly documented, and very subject to changes. Caveat
|
|
||||||
Maxima Emptor.
|
|
||||||
|
|
||||||
# Synopsis
|
|
||||||
|
|
||||||
```
|
|
||||||
import updux from 'updux';
|
|
||||||
|
|
||||||
import otherUpdux from './otherUpdux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
initial,
|
|
||||||
reducer,
|
|
||||||
actions,
|
|
||||||
middleware,
|
|
||||||
createStore,
|
|
||||||
} = new Updux({
|
|
||||||
initial: {
|
|
||||||
counter: 0,
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
otherUpdux,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
inc: ( increment = 1 ) => u({counter: s => s + increment })
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
'*' => api => next => action => {
|
|
||||||
console.log( "hey, look, an action zoomed by!", action );
|
|
||||||
next(action);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
customAction: ( someArg ) => ({
|
|
||||||
type: "custom",
|
|
||||||
payload: { someProp: someArg }
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = createStore();
|
|
||||||
|
|
||||||
store.dispatch.inc(3);
|
|
||||||
```
|
|
||||||
|
|
||||||
# Description
|
|
||||||
|
|
||||||
Full documentation can be [found here](https://yanick.github.io/updux/).
|
|
||||||
Right now the best way to understand the whole thing is to go
|
|
||||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
|
||||||
|
|
||||||
## 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 Updux instance as the default export:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
|
|
||||||
const updux = new Updux({ ... });
|
|
||||||
|
|
||||||
export default updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use them as subduxes like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
import foo from './foo'; // foo is an Updux
|
|
||||||
import bar from './bar'; // bar is an Updux as well
|
|
||||||
|
|
||||||
const updux = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
foo, bar
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if you want to use it:
|
|
||||||
|
|
||||||
```
|
|
||||||
import updux from './myUpdux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
reducer,
|
|
||||||
actions: { doTheThing },
|
|
||||||
createStore,
|
|
||||||
middleware,
|
|
||||||
} = updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mapping a mutation to all values of a state
|
|
||||||
|
|
||||||
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
|
||||||
enough to have the main reducer maps away all items to the sub-reducer:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({ initial: [] });
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.review,
|
|
||||||
(_,action) => state => state.map( todo.upreducer(action) )
|
|
||||||
);
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
But `updeep` can iterate through all the items of an array (or the values of
|
|
||||||
an object) via the special key `*`. So the todos updux above could also be
|
|
||||||
written:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({
|
|
||||||
subduxes: { '*': todo },
|
|
||||||
});
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The advantages being that the actions/mutations/effects of the subdux will be
|
|
||||||
imported by the root updux as usual, and all actions that aren't being
|
|
||||||
overridden by a sink mutation will trickle down 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: 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
@ -1,435 +0,0 @@
|
|||||||
[updux](../README.md) / [Exports](../modules.md) / default
|
|
||||||
|
|
||||||
# Class: default\<D\>
|
|
||||||
|
|
||||||
## Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `D` | extends `DuxConfig` |
|
|
||||||
|
|
||||||
## Constructors
|
|
||||||
|
|
||||||
### constructor
|
|
||||||
|
|
||||||
• **new default**\<`D`\>(`duxConfig`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `D` | extends `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: `Record`\<`string`, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<string, any\>; }\>\> }\> |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `duxConfig` | `D` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
### #defaultMutation
|
|
||||||
|
|
||||||
• `Private` **#defaultMutation**: `any`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### #effects
|
|
||||||
|
|
||||||
• `Private` **#effects**: `any`[] = `[]`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### #mutations
|
|
||||||
|
|
||||||
• `Private` **#mutations**: `any`[] = `[]`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### #reactions
|
|
||||||
|
|
||||||
• `Private` **#reactions**: `any`[] = `[]`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### duxConfig
|
|
||||||
|
|
||||||
• `Private` `Readonly` **duxConfig**: `D`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### memoBuildActions
|
|
||||||
|
|
||||||
• **memoBuildActions**: `Moized`\<(`localActions`: {}, `subduxes`: {}) => `any`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localActions`: {}, `subduxes`: {}) => `any`\> ; `onCacheChange`: `OnCacheOperation`\<(`localActions`: {}, `subduxes`: {}) => `any`\> ; `onCacheHit`: `OnCacheOperation`\<(`localActions`: {}, `subduxes`: {}) => `any`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### memoBuildEffects
|
|
||||||
|
|
||||||
• **memoBuildEffects**: `Moized`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[], `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[]\> ; `onCacheChange`: `OnCacheOperation`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[]\> ; `onCacheHit`: `OnCacheOperation`\<(`localEffects`: `any`, `subduxes`: {}) => `any`[]\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### memoBuildReducer
|
|
||||||
|
|
||||||
• **memoBuildReducer**: `Moized`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`\> ; `onCacheChange`: `OnCacheOperation`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`\> ; `onCacheHit`: `OnCacheOperation`\<(`initialStateState`: `unknown`, `mutations`: `MutationCase`[], `defaultMutation?`: `Omit`\<`MutationCase`, ``"matcher"``\>, `subduxes`: `Record`\<`string`, `Partial`\<\{ `actions`: `Record`\<`string`, `any`\> ; `initialState`: `any` ; `reducer`: `unknown` ; `schema`: `Record`\<`string`, `any`\> ; `selectors`: `Record`\<`string`, `any`\> ; `subduxes`: Record\<string, Partial\<\{ initialState: any; schema: Record\<string, any\>; actions: Record\<string, any\>; subduxes: Record\<string, Partial\<...\>\>; reducer: unknown; selectors: Record\<...\>; }\>\> }\>\>) => (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### memoBuildSchema
|
|
||||||
|
|
||||||
• **memoBuildSchema**: `Moized`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`\> ; `onCacheChange`: `OnCacheOperation`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`\> ; `onCacheHit`: `OnCacheOperation`\<(`schema`: `any`, `initialState`: `any`, `subduxes`: {}) => `any`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### memoBuildSelectors
|
|
||||||
|
|
||||||
• **memoBuildSelectors**: `Moized`\<(`localSelectors`: {}, `subduxes`: {}) => `object`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localSelectors`: {}, `subduxes`: {}) => `object`\> ; `onCacheChange`: `OnCacheOperation`\<(`localSelectors`: {}, `subduxes`: {}) => `object`\> ; `onCacheHit`: `OnCacheOperation`\<(`localSelectors`: {}, `subduxes`: {}) => `object`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### memoInitialState
|
|
||||||
|
|
||||||
• **memoInitialState**: `Moized`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`, `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`\> ; `onCacheChange`: `OnCacheOperation`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`\> ; `onCacheHit`: `OnCacheOperation`\<(`localInitialState`: `any`, `subduxes`: `any`) => `any`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & `Partial`\<\{ `isDeepEqual`: `boolean` ; `isPromise`: `boolean` ; `isReact`: `boolean` ; `isSerialized`: `boolean` ; `isShallowEqual`: `boolean` ; `matchesArg`: `IsEqual` ; `matchesKey`: `IsMatchingKey` ; `maxAge`: `number` ; `maxArgs`: `number` ; `maxSize`: `number` ; `onCacheAdd`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheChange`: `OnCacheOperation`\<`Moizeable`\> ; `onCacheHit`: `OnCacheOperation`\<`Moizeable`\> ; `onExpire`: `OnExpire` ; `profileName`: `string` ; `serializer`: `Serialize` ; `transformArgs`: `TransformKey` ; `updateCacheForKey`: `UpdateCacheForKey` ; `updateExpire`: `boolean` }\> & \{ `maxSize`: `number` = 1 }\>
|
|
||||||
|
|
||||||
## Accessors
|
|
||||||
|
|
||||||
### actions
|
|
||||||
|
|
||||||
• `get` **actions**(): `DuxActions`\<`D`\>
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`DuxActions`\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### asDux
|
|
||||||
|
|
||||||
• `get` **asDux**(): `Object`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Object`
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actions` | `DuxActions`\<`D`\> |
|
|
||||||
| `effects` | `any` |
|
|
||||||
| `initialState` | `DuxState`\<`D`\> |
|
|
||||||
| `reactions` | `any`[] |
|
|
||||||
| `reducer` | (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown` |
|
|
||||||
| `schema` | `any` |
|
|
||||||
| `selectors` | `DuxSelectors`\<`D`\> |
|
|
||||||
| `upreducer` | (`action`: `any`) => (`state`: `any`) => `unknown` |
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### effects
|
|
||||||
|
|
||||||
• `get` **effects**(): `any`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### foo
|
|
||||||
|
|
||||||
• `get` **foo**(): `DuxActions`\<`D`\>
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`DuxActions`\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### initialState
|
|
||||||
|
|
||||||
• `get` **initialState**(): `DuxState`\<`D`\>
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`DuxState`\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### reactions
|
|
||||||
|
|
||||||
• `get` **reactions**(): `any`[]
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`[]
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### reducer
|
|
||||||
|
|
||||||
• `get` **reducer**(): (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`state?`, `action`): `unknown`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type | Default value |
|
|
||||||
| :------ | :------ | :------ |
|
|
||||||
| `state` | `unknown` | `initialStateState` |
|
|
||||||
| `action` | `Action`\<`any`\> | `undefined` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`unknown`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### schema
|
|
||||||
|
|
||||||
• `get` **schema**(): `any`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### selectors
|
|
||||||
|
|
||||||
• `get` **selectors**(): `DuxSelectors`\<`D`\>
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`DuxSelectors`\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### upreducer
|
|
||||||
|
|
||||||
• `get` **upreducer**(): (`action`: `any`) => (`state`: `any`) => `unknown`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`action`): (`state`: `any`) => `unknown`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `action` | `any` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`state`): `unknown`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `state` | `any` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`unknown`
|
|
||||||
|
|
||||||
## Methods
|
|
||||||
|
|
||||||
### addDefaultMutation
|
|
||||||
|
|
||||||
▸ **addDefaultMutation**(`mutation`, `terminal?`): `any`
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `mutation` | `Mutation`\<`any`, `DuxState`\<`D`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### addEffect
|
|
||||||
|
|
||||||
▸ **addEffect**(`actionType`, `effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionType` | keyof `D` extends \{ `actions`: `A` } ? \{ [key in string \| number \| symbol]: key extends string ? ExpandedAction\<A[key], key\> : never } : {} \| keyof `UnionToIntersection`\<`D` extends \{ `subduxes`: `S` } ? `DuxActions`\<`S`[keyof `S`]\> : {}\> |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addEffect**(`actionCreator`, `effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionCreator` | `Object` |
|
|
||||||
| `actionCreator.match` | (`action`: `any`) => `boolean` |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addEffect**(`guardFunc`, `effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `guardFunc` | (`action`: `AnyAction`) => `boolean` |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addEffect**(`effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### addMutation
|
|
||||||
|
|
||||||
▸ **addMutation**\<`A`\>(`matcher`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `A` | extends `string` \| `number` \| `symbol` |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `matcher` | `A` |
|
|
||||||
| `mutation` | `Mutation`\<`DuxActions`\<`D`\>[`A`] extends (...`args`: `any`) => `P` ? `P` : `never`, `DuxState`\<`D`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addMutation**\<`A`\>(`matcher`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `A` | extends `Action`\<`any`, `A`\> |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `matcher` | (`action`: `A`) => `boolean` |
|
|
||||||
| `mutation` | `Mutation`\<`A`, `DuxState`\<`D`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addMutation**\<`A`\>(`actionCreator`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `A` | extends `ActionCreator`\<`any`, `any`[], `A`\> |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionCreator` | `A` |
|
|
||||||
| `mutation` | `Mutation`\<`ReturnType`\<`A`\>, `DuxState`\<`D`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### addReaction
|
|
||||||
|
|
||||||
▸ **addReaction**(`reaction`): `void`
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `reaction` | `any` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`void`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### createStore
|
|
||||||
|
|
||||||
▸ **createStore**(`options?`): `ToolkitStore`\<`D` extends \{ `initialState`: `INITIAL_STATE` } ? `INITIAL_STATE` : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown` & {}, `AnyAction`, `Middlewares`\<`DuxState`\<`D`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `DuxState`\<`D`\>\> & \{ `actions`: `DuxActions`\<`D`\> ; `dispatch`: `DuxActions`\<`D`\> ; `getState`: `CurriedSelectors`\<`DuxSelectors`\<`D`\>\> ; `selectors`: `DuxSelectors`\<`D`\> }
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `options` | `Partial`\<\{ `buildMiddleware`: (`middleware`: `any`[]) => `any` ; `preloadedState`: `DuxState`\<`D`\> ; `validate`: `boolean` }\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`ToolkitStore`\<`D` extends \{ `initialState`: `INITIAL_STATE` } ? `INITIAL_STATE` : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown` & {}, `AnyAction`, `Middlewares`\<`DuxState`\<`D`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `DuxState`\<`D`\>\> & \{ `actions`: `DuxActions`\<`D`\> ; `dispatch`: `DuxActions`\<`D`\> ; `getState`: `CurriedSelectors`\<`DuxSelectors`\<`D`\>\> ; `selectors`: `DuxSelectors`\<`D`\> }
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### toDux
|
|
||||||
|
|
||||||
▸ **toDux**(): `Object`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Object`
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actions` | `DuxActions`\<`D`\> |
|
|
||||||
| `effects` | `any` |
|
|
||||||
| `initialState` | `DuxState`\<`D`\> |
|
|
||||||
| `reactions` | `any`[] |
|
|
||||||
| `reducer` | (`state`: `unknown`, `action`: `Action`\<`any`\>) => `unknown` |
|
|
||||||
| `schema` | `any` |
|
|
||||||
| `selectors` | `DuxSelectors`\<`D`\> |
|
|
||||||
| `upreducer` | (`action`: `any`) => (`state`: `any`) => `unknown` |
|
|
@ -1,129 +0,0 @@
|
|||||||
[updux](README.md) / Exports
|
|
||||||
|
|
||||||
# updux
|
|
||||||
|
|
||||||
## Classes
|
|
||||||
|
|
||||||
- [default](classes/default.md)
|
|
||||||
|
|
||||||
## Functions
|
|
||||||
|
|
||||||
### createAction
|
|
||||||
|
|
||||||
▸ **createAction**\<`P`, `T`\>(`type`): `PayloadActionCreator`\<`P`, `T`\>
|
|
||||||
|
|
||||||
A utility function to create an action creator for the given action type
|
|
||||||
string. The action creator accepts a single argument, which will be included
|
|
||||||
in the action object as a field called payload. The action creator function
|
|
||||||
will also have its toString() overridden so that it returns the action type,
|
|
||||||
allowing it to be used in reducer logic that is looking for that action type.
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `P` | `void` |
|
|
||||||
| `T` | extends `string` = `string` |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
| :------ | :------ | :------ |
|
|
||||||
| `type` | `T` | The action type to use for created actions. |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`PayloadActionCreator`\<`P`, `T`\>
|
|
||||||
|
|
||||||
▸ **createAction**\<`PA`, `T`\>(`type`, `prepareAction`): `PayloadActionCreator`\<`ReturnType`\<`PA`\>[``"payload"``], `T`, `PA`\>
|
|
||||||
|
|
||||||
A utility function to create an action creator for the given action type
|
|
||||||
string. The action creator accepts a single argument, which will be included
|
|
||||||
in the action object as a field called payload. The action creator function
|
|
||||||
will also have its toString() overridden so that it returns the action type,
|
|
||||||
allowing it to be used in reducer logic that is looking for that action type.
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `PA` | extends `PrepareAction`\<`any`\> |
|
|
||||||
| `T` | extends `string` = `string` |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
| :------ | :------ | :------ |
|
|
||||||
| `type` | `T` | The action type to use for created actions. |
|
|
||||||
| `prepareAction` | `PA` | - |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`PayloadActionCreator`\<`ReturnType`\<`PA`\>[``"payload"``], `T`, `PA`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### withPayload
|
|
||||||
|
|
||||||
▸ **withPayload**\<`P`\>(): (`input`: `P`) => \{ `payload`: `P` }
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name |
|
|
||||||
| :------ |
|
|
||||||
| `P` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`input`): `Object`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `input` | `P` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`Object`
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `payload` | `P` |
|
|
||||||
|
|
||||||
▸ **withPayload**\<`P`, `A`\>(`prepare`): (...`input`: `A`) => \{ `payload`: `P` }
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `P` | `P` |
|
|
||||||
| `A` | extends `any`[] |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `prepare` | (...`args`: `A`) => `P` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`...input`): `Object`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `...input` | `A` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`Object`
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `payload` | `P` |
|
|
@ -1,115 +0,0 @@
|
|||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
--8<- "docs/tutorial-effects.test.ts:effects-1"
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Subduxes
|
|
||||||
|
|
||||||
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 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 dux we have so far:
|
|
||||||
|
|
||||||
```title="todos-monolith.ts"
|
|
||||||
--8<- "docs/tutorial-monolith.test.ts:mono"
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### NextId dux
|
|
||||||
|
|
||||||
```js title="nextId.ts"
|
|
||||||
--8<- "docs/nextId.ts:dux"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Todo updux
|
|
||||||
|
|
||||||
```title="todo.ts"
|
|
||||||
--8<- "docs/todo.ts"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Todos updux
|
|
||||||
|
|
||||||
```title="todos.ts"
|
|
||||||
--8<- "docs/todos.ts"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Main store
|
|
||||||
|
|
||||||
```title="todoList.ts"
|
|
||||||
--8<- "docs/todoList.ts"
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## 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:
|
|
||||||
|
|
||||||
```
|
|
||||||
(storeApi) => (state, previousState, unsubscribe) => {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Subscriptions registered for an updux and its subduxes are automatically
|
|
||||||
subscribed to the store when calling `createStore`.
|
|
||||||
|
|
||||||
The `state` passed to the subscriptions of the subduxes is the local state.
|
|
||||||
|
|
||||||
Also, all subscriptions are wrapped such that they are called only if the
|
|
||||||
local `state` changed since their last invocation.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todos = new Updux({
|
|
||||||
initial: [],
|
|
||||||
actions: {
|
|
||||||
setNbrTodos: null,
|
|
||||||
addTodo: null,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
addTodo: todo => todos => [ ...todos, todo ],
|
|
||||||
},
|
|
||||||
reactions: [
|
|
||||||
({dispatch}) => todos => dispatch.setNbrTodos(todos.length)
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const myDux = new Updux({
|
|
||||||
initial: {
|
|
||||||
nbrTodos: 0
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
todos,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
setNbrTodos: nbrTodos => u({ nbrTodos })
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
[immer]: https://www.npmjs.com/package/immer
|
|
||||||
[lodash]: https://www.npmjs.com/package/lodash
|
|
||||||
[ts-action]: https://www.npmjs.com/package/ts-action
|
|
||||||
[remeda]: remedajs.com/
|
|
21
docs/.gitignore
vendored
21
docs/.gitignore
vendored
@ -1,21 +0,0 @@
|
|||||||
# build output
|
|
||||||
dist/
|
|
||||||
# generated types
|
|
||||||
.astro/
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# logs
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
|
||||||
.env
|
|
||||||
.env.production
|
|
||||||
|
|
||||||
# macOS-specific files
|
|
||||||
.DS_Store
|
|
4
docs/.vscode/extensions.json
vendored
4
docs/.vscode/extensions.json
vendored
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
11
docs/.vscode/launch.json
vendored
11
docs/.vscode/launch.json
vendored
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
243
docs/README.md
243
docs/README.md
@ -1,55 +1,226 @@
|
|||||||
# Starlight Starter Kit: Basics
|
# What's Updux?
|
||||||
|
|
||||||
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
|
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
|
||||||
|
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
|
||||||
|
|
||||||
|
It has a couple of pretty good ideas that removes some of the
|
||||||
|
boilerplate. 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.
|
||||||
|
|
||||||
|
All that to say, say hello to `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
|
||||||
|
effects (middleware reacting to actions that can be asynchronous and/or
|
||||||
|
have side-effects), so everything pertaining to a store are all defined
|
||||||
|
in the space place.
|
||||||
|
- Automatically gather all actions used by the updux's effects and mutations,
|
||||||
|
and makes then accessible as attributes to the `dispatch` object of the
|
||||||
|
store.
|
||||||
|
- Mutations have a signature that is friendly to Updux and Immer.
|
||||||
|
- Also, the mutation signature auto-unwrap the payload of the actions for you.
|
||||||
|
- TypeScript types.
|
||||||
|
|
||||||
|
Fair warning: this package is still very new, probably very buggy,
|
||||||
|
definitively very badly documented, and very subject to changes. Caveat
|
||||||
|
Maxima Emptor.
|
||||||
|
|
||||||
|
# Synopsis
|
||||||
|
|
||||||
```
|
```
|
||||||
npm create astro@latest -- --template starlight
|
import updux from 'updux';
|
||||||
|
|
||||||
|
import otherUpdux from './otherUpdux';
|
||||||
|
|
||||||
|
const {
|
||||||
|
initial,
|
||||||
|
reducer,
|
||||||
|
actions,
|
||||||
|
middleware,
|
||||||
|
createStore,
|
||||||
|
} = new Updux({
|
||||||
|
initial: {
|
||||||
|
counter: 0,
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
otherUpdux,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
inc: ( increment = 1 ) => u({counter: s => s + increment })
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
'*' => api => next => action => {
|
||||||
|
console.log( "hey, look, an action zoomed by!", action );
|
||||||
|
next(action);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
customAction: ( someArg ) => ({
|
||||||
|
type: "custom",
|
||||||
|
payload: { someProp: someArg }
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = createStore();
|
||||||
|
|
||||||
|
store.dispatch.inc(3);
|
||||||
```
|
```
|
||||||
|
|
||||||
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
|
# Description
|
||||||
[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
|
|
||||||
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
|
|
||||||
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
Full documentation can be [found here](https://yanick.github.io/updux/).
|
||||||
|
Right now the best way to understand the whole thing is to go
|
||||||
|
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
||||||
|
|
||||||
## 🚀 Project Structure
|
## Exporting upduxes
|
||||||
|
|
||||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
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 Updux instance as the default export:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
import Updux from 'updux';
|
||||||
├── public/
|
|
||||||
├── src/
|
const updux = new Updux({ ... });
|
||||||
│ ├── assets/
|
|
||||||
│ ├── content/
|
export default updux;
|
||||||
│ │ ├── docs/
|
|
||||||
│ │ └── config.ts
|
|
||||||
│ └── env.d.ts
|
|
||||||
├── astro.config.mjs
|
|
||||||
├── package.json
|
|
||||||
└── tsconfig.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
|
Then you can use them as subduxes like this:
|
||||||
|
|
||||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
```
|
||||||
|
import Updux from 'updux';
|
||||||
|
import foo from './foo'; // foo is an Updux
|
||||||
|
import bar from './bar'; // bar is an Updux as well
|
||||||
|
|
||||||
Static assets, like favicons, can be placed in the `public/` directory.
|
const updux = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
foo, bar
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## 🧞 Commands
|
Or if you want to use it:
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
```
|
||||||
|
import updux from './myUpdux';
|
||||||
|
|
||||||
| Command | Action |
|
const {
|
||||||
| :------------------------ | :----------------------------------------------- |
|
reducer,
|
||||||
| `npm install` | Installs dependencies |
|
actions: { doTheThing },
|
||||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
createStore,
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
middleware,
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
} = updux;
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
```
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
## Mapping a mutation to all values of a state
|
||||||
|
|
||||||
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
|
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
||||||
|
enough to have the main reducer maps away all items to the sub-reducer:
|
||||||
|
|
||||||
|
```
|
||||||
|
const todo = new Updux({
|
||||||
|
mutations: {
|
||||||
|
review: () => u({ reviewed: true}),
|
||||||
|
done: () => u({done: true}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const todos = new Updux({ initial: [] });
|
||||||
|
|
||||||
|
todos.addMutation(
|
||||||
|
todo.actions.review,
|
||||||
|
(_,action) => state => state.map( todo.upreducer(action) )
|
||||||
|
);
|
||||||
|
todos.addMutation(
|
||||||
|
todo.actions.done,
|
||||||
|
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
||||||
|
);
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
But `updeep` can iterate through all the items of an array (or the values of
|
||||||
|
an object) via the special key `*`. So the todos updux above could also be
|
||||||
|
written:
|
||||||
|
|
||||||
|
```
|
||||||
|
const todo = new Updux({
|
||||||
|
mutations: {
|
||||||
|
review: () => u({ reviewed: true}),
|
||||||
|
done: () => u({done: true}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const todos = new Updux({
|
||||||
|
subduxes: { '*': todo },
|
||||||
|
});
|
||||||
|
|
||||||
|
todos.addMutation(
|
||||||
|
todo.actions.done,
|
||||||
|
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The advantages being that the actions/mutations/effects of the subdux will be
|
||||||
|
imported by the root updux as usual, and all actions that aren't being
|
||||||
|
overridden by a sink mutation will trickle down 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: 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
import { defineConfig, squooshImageService } from 'astro/config';
|
|
||||||
import starlight from '@astrojs/starlight';
|
|
||||||
|
|
||||||
// https://astro.build/config
|
|
||||||
export default defineConfig({
|
|
||||||
image: { service: squooshImageService() },
|
|
||||||
integrations: [
|
|
||||||
starlight({
|
|
||||||
title: 'Updux',
|
|
||||||
pagefind: false,
|
|
||||||
social: {
|
|
||||||
github: 'https://github.com/withastro/starlight',
|
|
||||||
},
|
|
||||||
sidebar: [
|
|
||||||
{
|
|
||||||
label: 'Home',
|
|
||||||
slug: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Tutorial',
|
|
||||||
autogenerate: {
|
|
||||||
directory: 'tutorial',
|
|
||||||
}
|
|
||||||
// items: [
|
|
||||||
// { label: 'Introduction', slug: 'tutorial/intro'}
|
|
||||||
// { label: 'Introduction', slug: 'tutorial/intro'}
|
|
||||||
// ]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'API',
|
|
||||||
items: [
|
|
||||||
{ label: 'updux', slug: 'api/modules' },
|
|
||||||
{ label: 'defaults', slug: 'api/classes/default' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Guide',
|
|
||||||
autogenerate: {
|
|
||||||
directory: 'guides',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// items: [
|
|
||||||
// // Each item here is one entry in the navigation menu.
|
|
||||||
// { label: 'Example Guide', slug: 'guides/example' },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// label: 'Reference',
|
|
||||||
// autogenerate: { directory: 'reference' },
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
@ -6,7 +6,7 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
<meta name="description" content="Description">
|
<meta name="description" content="Description">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||||
<link rel="stylesheet" href="docsify/themes/vue.css">
|
<link rel="stylesheet" href="docsify/themes/buble.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
19
docs/nextId.ts
Normal file
19
docs/nextId.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/// [dux]
|
||||||
|
import Updux from '../src/index.js';
|
||||||
|
import u from '@yanick/updeep-remeda';
|
||||||
|
|
||||||
|
const nextIdDux = new Updux({
|
||||||
|
initialState: 1,
|
||||||
|
actions: {
|
||||||
|
incNextId: null,
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
getNextId: state => state
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
nextIdDux.addMutation('incNextId', () => id => id + 1)
|
||||||
|
|
||||||
|
export default nextIdDux.asDux;
|
||||||
|
|
||||||
|
/// [dux]
|
@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "starlight-docs",
|
|
||||||
"type": "module",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "astro dev",
|
|
||||||
"start": "astro dev",
|
|
||||||
"build": "astro build",
|
|
||||||
"preview": "astro preview",
|
|
||||||
"astro": "astro"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@astrojs/starlight": "^0.25.3",
|
|
||||||
"astro": "^4.10.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vitest": "^2.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 583 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 583 KiB |
53
docs/recipes.test.js
Normal file
53
docs/recipes.test.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
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
|
||||||
|
]);
|
||||||
|
|
||||||
|
});
|
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
@ -1,6 +0,0 @@
|
|||||||
import { defineCollection } from 'astro:content';
|
|
||||||
import { docsSchema } from '@astrojs/starlight/schema';
|
|
||||||
|
|
||||||
export const collections = {
|
|
||||||
docs: defineCollection({ schema: docsSchema() }),
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
|
|
@ -1,153 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Description"
|
|
||||||
---
|
|
||||||
|
|
||||||
Full documentation can be [found here](https://yanick.github.io/updux/).
|
|
||||||
Right now the best way to understand the whole thing is to go
|
|
||||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
|
||||||
|
|
||||||
## 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 Updux instance as the default export:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
|
|
||||||
const updux = new Updux({ ... });
|
|
||||||
|
|
||||||
export default updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use them as subduxes like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
import foo from './foo'; // foo is an Updux
|
|
||||||
import bar from './bar'; // bar is an Updux as well
|
|
||||||
|
|
||||||
const updux = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
foo, bar
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if you want to use it:
|
|
||||||
|
|
||||||
```
|
|
||||||
import updux from './myUpdux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
reducer,
|
|
||||||
actions: { doTheThing },
|
|
||||||
createStore,
|
|
||||||
middleware,
|
|
||||||
} = updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mapping a mutation to all values of a state
|
|
||||||
|
|
||||||
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
|
||||||
enough to have the main reducer maps away all items to the sub-reducer:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({ initial: [] });
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.review,
|
|
||||||
(_,action) => state => state.map( todo.upreducer(action) )
|
|
||||||
);
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
But `updeep` can iterate through all the items of an array (or the values of
|
|
||||||
an object) via the special key `*`. So the todos updux above could also be
|
|
||||||
written:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({
|
|
||||||
subduxes: { '*': todo },
|
|
||||||
});
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The advantages being that the actions/mutations/effects of the subdux will be
|
|
||||||
imported by the root updux as usual, and all actions that aren't being
|
|
||||||
overridden by a sink mutation will trickle down 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: 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
@ -1,355 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Class: default<D>"
|
|
||||||
---
|
|
||||||
|
|
||||||
**`Description`**
|
|
||||||
|
|
||||||
main Updux class
|
|
||||||
|
|
||||||
## Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `D` | extends `DuxConfig` |
|
|
||||||
|
|
||||||
## Constructors
|
|
||||||
|
|
||||||
### constructor
|
|
||||||
|
|
||||||
• **new default**\<`D`\>(`duxConfig`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `D` | extends `Partial`\<\{ `actions`: `Record`\<`string`, `Function` \| `ActionCreator`\<`string`, `any`[]\>\> ; `initialState`: `any` ; `selectors`: `Record`\<`string`, `Function`\> ; `subduxes`: `Record`\<`string`, Partial\<\{ initialState: any; subduxes: Record\<string, Partial\<...\>\>; actions: Record\<string, Function \| ActionCreator\<string, any[]\>\>; selectors: Record\<...\>; }\>\> }\> |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `duxConfig` | `D` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
## Accessors
|
|
||||||
|
|
||||||
### actions
|
|
||||||
|
|
||||||
• `get` **actions**(): `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### effects
|
|
||||||
|
|
||||||
• `get` **effects**(): `any`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### initialState
|
|
||||||
|
|
||||||
• `get` **initialState**(): `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>
|
|
||||||
|
|
||||||
**`Description`**
|
|
||||||
|
|
||||||
Initial state of the dux.
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### reactions
|
|
||||||
|
|
||||||
• `get` **reactions**(): `any`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### reducer
|
|
||||||
|
|
||||||
• `get` **reducer**(): `any`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### selectors
|
|
||||||
|
|
||||||
• `get` **selectors**(): `ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\>
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### subduxes
|
|
||||||
|
|
||||||
• `get` **subduxes**(): `Object`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Object`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### upreducer
|
|
||||||
|
|
||||||
• `get` **upreducer**(): (`action`: `AnyAction`) => (`state?`: `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>) => `any`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`action`): (`state?`: `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>) => `any`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `action` | `AnyAction` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`state?`): `any`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `state?` | `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
## Methods
|
|
||||||
|
|
||||||
### addAction
|
|
||||||
|
|
||||||
▸ **addAction**(`action`): `void`
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `action` | `ActionCreator`\<`any`, `any`, `any`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`void`
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### addEffect
|
|
||||||
|
|
||||||
▸ **addEffect**\<`A`\>(`actionType`, `effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `A` | extends `string` \| `number` \| `symbol` |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionType` | `A` |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`, `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>[`A`] extends (...`args`: `any`[]) => `R` ? `R` : `AnyAction`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addEffect**\<`AC`\>(`actionCreator`, `effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `AC` | extends `ActionCreatorWithPreparedPayload`\<`any`, `any`, `string`, `never`, `never`, `AC`\> |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionCreator` | `AC` |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`, `ReturnType`\<`AC`\>\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addEffect**(`guardFunc`, `effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `guardFunc` | (`action`: `AnyAction`) => `boolean` |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`, `AnyAction`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addEffect**(`effect`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `effect` | `EffectMiddleware`\<`D`, `AnyAction`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### addMutation
|
|
||||||
|
|
||||||
▸ **addMutation**\<`A`\>(`actionType`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `A` | extends `string` \| `number` \| `symbol` |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionType` | `A` |
|
|
||||||
| `mutation` | `Mutation`\<`ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\>[`A`] extends (...`args`: `any`[]) => `R` ? `R` : `AnyAction`, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
▸ **addMutation**\<`AC`\>(`actionCreator`, `mutation`, `terminal?`): [`default`](default.md)\<`D` & \{ `actions`: `Record`\<`AC` extends \{ `type`: `T` } ? `T` : `never`, `AC`\> }\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `AC` | extends `ActionCreatorWithPreparedPayload`\<`any`, `any`, `string`, `never`, `never`, `AC`\> |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionCreator` | `AC` |
|
|
||||||
| `mutation` | `Mutation`\<`ReturnType`\<`AC`\>, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D` & \{ `actions`: `Record`\<`AC` extends \{ `type`: `T` } ? `T` : `never`, `AC`\> }\>
|
|
||||||
|
|
||||||
▸ **addMutation**\<`A`, `P`, `X`\>(`actionCreator`, `mutation`, `terminal?`): [`default`](default.md)\<`D` & \{ `actions`: `Record`\<`A`, `ActionCreator`\<`A`, `P`, `X`\>\> }\>
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `A` | extends `string` |
|
|
||||||
| `P` | `P` |
|
|
||||||
| `X` | extends `any`[] |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `actionCreator` | `ActionCreator`\<`A`, `P`, `X`\> |
|
|
||||||
| `mutation` | `Mutation`\<\{ `payload`: `P` ; `type`: `A` }, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D` & \{ `actions`: `Record`\<`A`, `ActionCreator`\<`A`, `P`, `X`\>\> }\>
|
|
||||||
|
|
||||||
▸ **addMutation**(`matcher`, `mutation`, `terminal?`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `matcher` | (`action`: `AnyAction`) => `boolean` |
|
|
||||||
| `mutation` | `Mutation`\<`AnyAction`, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### addReaction
|
|
||||||
|
|
||||||
▸ **addReaction**(`reaction`): [`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `reaction` | `DuxReaction`\<`D`\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
[`default`](default.md)\<`D`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### createStore
|
|
||||||
|
|
||||||
▸ **createStore**(`options?`): `ToolkitStore`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> & {}, `AnyAction`, `Middlewares`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> & \{ `actions`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `dispatch`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `getState`: `CurriedSelectors`\<`ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, (...)[(...)]\> }\>\> : {}\>\> ; `selectors`: `ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\> }
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `options` | `Partial`\<\{ `preloadedState`: `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> }\> |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`ToolkitStore`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\> & {}, `AnyAction`, `Middlewares`\<`ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\>\> & `MiddlewareAPI`\<`Dispatch`\<`AnyAction`\>, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> & \{ `actions`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `dispatch`: `ResolveActions`\<`D` extends \{ `actions`: `any` } ? `D`[``"actions"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesActions`\<`D`\> : `unknown`\> ; `getState`: `CurriedSelectors`\<`ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, (...)[(...)]\> }\>\> : {}\>\> ; `selectors`: `ForceResolveObject`\<`D` extends \{ `selectors`: `S` } ? `S` : {} & `D` extends \{ `subduxes`: `SUB` } ? `UnionToIntersection`\<`Values`\<\{ [key in string \| number \| symbol]: RebaseSelectors\<key, SUB[key]\> }\>\> : {}\> }
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### setDefaultMutation
|
|
||||||
|
|
||||||
▸ **setDefaultMutation**(`mutation`, `terminal?`): `any`
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `mutation` | `Mutation`\<`any`, `ForceResolveObject`\<`D` extends \{ `initialState`: `any` } ? `D`[``"initialState"``] : {} & `D` extends \{ `subduxes`: `any` } ? `SubduxesState`\<`D`\> : `unknown`\>\> |
|
|
||||||
| `terminal?` | `boolean` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
@ -1,129 +0,0 @@
|
|||||||
---
|
|
||||||
title: "updux"
|
|
||||||
---
|
|
||||||
|
|
||||||
## Classes
|
|
||||||
|
|
||||||
- [default](classes/default.md)
|
|
||||||
|
|
||||||
## Functions
|
|
||||||
|
|
||||||
### createAction
|
|
||||||
|
|
||||||
▸ **createAction**\<`P`, `T`\>(`type`): `PayloadActionCreator`\<`P`, `T`\>
|
|
||||||
|
|
||||||
A utility function to create an action creator for the given action type
|
|
||||||
string. The action creator accepts a single argument, which will be included
|
|
||||||
in the action object as a field called payload. The action creator function
|
|
||||||
will also have its toString() overridden so that it returns the action type,
|
|
||||||
allowing it to be used in reducer logic that is looking for that action type.
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `P` | `void` |
|
|
||||||
| `T` | extends `string` = `string` |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
| :------ | :------ | :------ |
|
|
||||||
| `type` | `T` | The action type to use for created actions. |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`PayloadActionCreator`\<`P`, `T`\>
|
|
||||||
|
|
||||||
▸ **createAction**\<`PA`, `T`\>(`type`, `prepareAction`): `PayloadActionCreator`\<`ReturnType`\<`PA`\>[``"payload"``], `T`, `PA`\>
|
|
||||||
|
|
||||||
A utility function to create an action creator for the given action type
|
|
||||||
string. The action creator accepts a single argument, which will be included
|
|
||||||
in the action object as a field called payload. The action creator function
|
|
||||||
will also have its toString() overridden so that it returns the action type,
|
|
||||||
allowing it to be used in reducer logic that is looking for that action type.
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `PA` | extends `PrepareAction`\<`any`\> |
|
|
||||||
| `T` | extends `string` = `string` |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type | Description |
|
|
||||||
| :------ | :------ | :------ |
|
|
||||||
| `type` | `T` | The action type to use for created actions. |
|
|
||||||
| `prepareAction` | `PA` | - |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`PayloadActionCreator`\<`ReturnType`\<`PA`\>[``"payload"``], `T`, `PA`\>
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
### withPayload
|
|
||||||
|
|
||||||
▸ **withPayload**\<`P`\>(): (`input`: `P`) => \{ `payload`: `P` }
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name |
|
|
||||||
| :------ |
|
|
||||||
| `P` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`input`): `Object`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `input` | `P` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`Object`
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `payload` | `P` |
|
|
||||||
|
|
||||||
▸ **withPayload**\<`P`, `A`\>(`prepare`): (...`input`: `A`) => \{ `payload`: `P` }
|
|
||||||
|
|
||||||
#### Type parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `P` | `P` |
|
|
||||||
| `A` | extends `any`[] |
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `prepare` | (...`args`: `A`) => `P` |
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`fn`
|
|
||||||
|
|
||||||
▸ (`...input`): `Object`
|
|
||||||
|
|
||||||
##### Parameters
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `...input` | `A` |
|
|
||||||
|
|
||||||
##### Returns
|
|
||||||
|
|
||||||
`Object`
|
|
||||||
|
|
||||||
| Name | Type |
|
|
||||||
| :------ | :------ |
|
|
||||||
| `payload` | `P` |
|
|
@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
title: Introduction
|
|
||||||
---
|
|
||||||
|
|
||||||
Updux is a class that collects together all the stuff pertaining to a Redux
|
|
||||||
reducer -- actions, middleware, subscriptions, selectors -- in a way that
|
|
||||||
is as modular and as TypeScript-friendly as possible.
|
|
||||||
|
|
||||||
While it has originally been created to play well with [updeep][], it also
|
|
||||||
work wells with plain JavaScript, and
|
|
||||||
interfaces very neatly with other immutability/deep merging libraries
|
|
||||||
like
|
|
||||||
[Mutative], [immer][], [updeep][],
|
|
||||||
[remeda][],
|
|
||||||
[lodash][], etc.
|
|
||||||
|
|
||||||
## Updux terminology
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt>Updux</dt>
|
|
||||||
<dd>Object encapsulating the information pertinent for a Redux reducer.
|
|
||||||
Named thus because it has been designed to work well with [updeep][],
|
|
||||||
and follows my spin on
|
|
||||||
the [Ducks pattern](https://github.com/erikras/ducks-modular-redux).</dd>
|
|
||||||
<dt>Mutation</dt>
|
|
||||||
<dd>Reducing function, mostly associated with an action. All mutations of
|
|
||||||
an updux object are combined to form its reducer.</dd>
|
|
||||||
<dt>Subdux</dt>
|
|
||||||
<dd>Updux objects associated with sub-states of a main updux. The main
|
|
||||||
updux will inherit all of its subduxes actions, mutations, reactions,
|
|
||||||
etc.</dd>
|
|
||||||
<dt>Effect</dt>
|
|
||||||
<dd>A Redux middleware, augmented with a few goodies.</dd>
|
|
||||||
<dt>Reaction</dt>
|
|
||||||
<dd>Subscription to a updux. Unlike regular Redux subscriptions, don't
|
|
||||||
trigger if the state of the updux isn't changed by the reducing.</dd>
|
|
||||||
|
|
||||||
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
|
|
||||||
[updeep]: https://www.npmjs.com/package/@yanick/updeep-remeda
|
|
||||||
[immer]: https://www.npmjs.com/package/immer
|
|
||||||
[lodash]: https://www.npmjs.com/package/lodash
|
|
||||||
[ts-action]: https://www.npmjs.com/package/ts-action
|
|
||||||
[remeda]: remedajs.com/
|
|
||||||
[Mutative]: https://mutative.js.org/
|
|
@ -1,32 +0,0 @@
|
|||||||
---
|
|
||||||
title: State definition
|
|
||||||
---
|
|
||||||
|
|
||||||
The state of a Updux store is automatically interpolated from the constructor
|
|
||||||
argument `initialState` (which also provides the reducer's default initial state)..
|
|
||||||
|
|
||||||
```js
|
|
||||||
const myDux = new Updux({
|
|
||||||
initialState: {
|
|
||||||
counter: 1,
|
|
||||||
name: 'foo',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
If `initialState` is not provided, it defaults to an empty object.
|
|
||||||
|
|
||||||
## Manually providing the store state
|
|
||||||
|
|
||||||
Sometimes -- mostly when arrays are involved -- the initial state is not
|
|
||||||
providing the whole state type. In those cases specifying manually the type of
|
|
||||||
`initialState` will do the trick.
|
|
||||||
|
|
||||||
```js
|
|
||||||
type Todo = { label: string; done: boolean };
|
|
||||||
const initialState : Todo[] = [];
|
|
||||||
|
|
||||||
const myDux = new Updux({
|
|
||||||
initialState
|
|
||||||
});
|
|
||||||
```
|
|
@ -1,43 +0,0 @@
|
|||||||
---
|
|
||||||
title: Selectors
|
|
||||||
---
|
|
||||||
|
|
||||||
Selectors can be defined to get data derived from the state.
|
|
||||||
The selectors can be accessed as-is via the accessor `selectors`.
|
|
||||||
More interestingly, the `getState` method of a dux store will be augmented
|
|
||||||
with its selectors, with the first call for the state already
|
|
||||||
curried for you.
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
type Todo = { label: string; id: number; done: boolean };
|
|
||||||
|
|
||||||
const dux = new Updux({
|
|
||||||
initialState: [] as Todo[],
|
|
||||||
selectors: {
|
|
||||||
getDone: (state) => state.filter(({ done }) => done),
|
|
||||||
getById: (state) => (id) => state.find((todo) => todo.id === id),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const done = dux.selectors.getDone([ ... ]);
|
|
||||||
const myTodo = dux.selectors.getDone([...])(12);
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Store selectors
|
|
||||||
|
|
||||||
The store created by an updux will have the updux selectors available from a
|
|
||||||
`selectors` property. In addition, the store `getState` function will also have
|
|
||||||
the actions as properties.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const store = dux.createStore();
|
|
||||||
|
|
||||||
const done = store.selectors.getDone([ ... ]);
|
|
||||||
const myTodo = store.selectors.getDone([...])(12);
|
|
||||||
|
|
||||||
const done = store.getState.getDone();
|
|
||||||
const myTodo = store.getState.getDone(12);
|
|
||||||
```
|
|
@ -1,96 +0,0 @@
|
|||||||
---
|
|
||||||
title: Actions
|
|
||||||
---
|
|
||||||
|
|
||||||
Updux actions are just the usual Redux actions. Updux exports two
|
|
||||||
utility functions: `createAction`, which is a convenience re-export of the
|
|
||||||
function provided by
|
|
||||||
[@reduxjs/toolkit](https://redux-toolkit.js.org/), and
|
|
||||||
`withPayload`, an utility function that makes defining the
|
|
||||||
action payload just a little more concise.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const foo = createAction('foo', withPayload<string>() );
|
|
||||||
// equivalent to
|
|
||||||
const foo = createAction('foo', (payload: string) => ({ payload }) ) );
|
|
||||||
|
|
||||||
const createAction('foo', withPayload( (level:number) => ({ level }) );
|
|
||||||
// equivalent to
|
|
||||||
const createAction('foo', (level: number) => ({ payload: { level } }) ) );
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding actions to an Updux object
|
|
||||||
|
|
||||||
Actions can be tied to an Updux object both at construction time
|
|
||||||
|
|
||||||
```js
|
|
||||||
const addTodo = createAction('addTodo', withPayload<string>());
|
|
||||||
const todoDone = createAction('todoDone', withPayload<number>());
|
|
||||||
|
|
||||||
const myDux = new Updux({
|
|
||||||
actions: {
|
|
||||||
addTodo,
|
|
||||||
todoDone,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
or they will be automatically added when used in methods like `addMutation`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
|
|
||||||
const myDux = new Updux({})
|
|
||||||
.addMutation( addTodo, (label) => todos => todos.concat({ label });
|
|
||||||
```
|
|
||||||
|
|
||||||
:::danger
|
|
||||||
|
|
||||||
Note the chained invocation of `addMutation`, which is needed if you want
|
|
||||||
the type of myDux to include the `myAction` action.
|
|
||||||
|
|
||||||
```js
|
|
||||||
|
|
||||||
// myDux will have the right type for myDux.actions.myAction
|
|
||||||
myDux = new Updux({})
|
|
||||||
.addMutation( myAction, (payload) => state => state + payload );
|
|
||||||
|
|
||||||
// myDux type will not know about myAction.
|
|
||||||
myDux = new Updux({});
|
|
||||||
myDux.addMutation( myAction, (payload) => state => state + payload );
|
|
||||||
|
|
||||||
``````
|
|
||||||
:::
|
|
||||||
|
|
||||||
### Accessing Updux actions
|
|
||||||
|
|
||||||
Once an action is defined, its creator is accessible via the `actions` accessor.
|
|
||||||
|
|
||||||
```js
|
|
||||||
myDux.actions.addTodo('write tutorial');
|
|
||||||
// => { type: 'addtodo', payload: 'write tutorial' }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Store actions
|
|
||||||
|
|
||||||
The store created by an updux will have the updux actions available from a
|
|
||||||
`actions` property. In addition, the store `dispatch` function will also have
|
|
||||||
the actions as properties, which can be used to create the actions and
|
|
||||||
dispatch them immediately.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const myDux = new Updux({
|
|
||||||
actions: {
|
|
||||||
foo: (x: string) => x,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = myDux.createStore();
|
|
||||||
|
|
||||||
const a = store.actions.foo('bar'); // { type: 'foo', payload: 'bar' }
|
|
||||||
|
|
||||||
store.dispatch.foo('bar');
|
|
||||||
// equivalent to
|
|
||||||
// store.dispatch( store.actions.foo('bar') );
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
@ -1,86 +0,0 @@
|
|||||||
---
|
|
||||||
title: Mutations
|
|
||||||
---
|
|
||||||
|
|
||||||
Mutations are what tie actions to a reducing function.
|
|
||||||
Their signature is a smidge different than the typical
|
|
||||||
Redux reducing functions:
|
|
||||||
|
|
||||||
```js
|
|
||||||
( actionPayload, action ) => state => newState
|
|
||||||
```
|
|
||||||
|
|
||||||
## Defining a mutation
|
|
||||||
|
|
||||||
|
|
||||||
They are tied to actions in an updux via the method `addMutation`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
todosDux.addMutation(addTodo, (description) => ({ todos, nextId }) => ({
|
|
||||||
nextId: 1 + nextId,
|
|
||||||
todos: todos.concat({ description, id: nextId, done: false }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
todosDux.addMutation(todoDone, (id) => ({ todos, nextId }) => ({
|
|
||||||
nextId: 1 + nextId,
|
|
||||||
todos: todos.map((todo) => {
|
|
||||||
if (todo.id !== id) return todo;
|
|
||||||
return {
|
|
||||||
...todo,
|
|
||||||
done: true,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
```
|
|
||||||
|
|
||||||
The first argument can be an action creator (the action will be added to the
|
|
||||||
actions known to the updux), an action type (must be the type of an action
|
|
||||||
already known to the updux), a function taking in an action and returning a
|
|
||||||
boolean.
|
|
||||||
|
|
||||||
```
|
|
||||||
const addTodo = createAction( 'addTodo', withPayload<string>() );
|
|
||||||
const todoDone = createAction( 'todoDone', withPayload<number>() )
|
|
||||||
|
|
||||||
const myDux = new Updux({
|
|
||||||
actions: {
|
|
||||||
addTodo
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addMutation( addTodo, ... );
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addMutation( 'addTodo', ... );
|
|
||||||
|
|
||||||
// invalid! todoDone is not known to myDux
|
|
||||||
myDux.addMutation( 'todoDone', ... )
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addMutation( todoDone, ... )
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addMutation(
|
|
||||||
(action) => action.type.startsWidth('todo'),
|
|
||||||
...
|
|
||||||
);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The first argument can also be omitted to have a mutation that
|
|
||||||
is applied for all incoming actions.
|
|
||||||
|
|
||||||
```js
|
|
||||||
myDux.addMutation( (payload,action) => state => state+1 );
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Default mutation
|
|
||||||
|
|
||||||
It's possible to set a mutation that will be triggered if an action doesn't match
|
|
||||||
any of the other updux mutations (excluding subdux mutations).
|
|
||||||
|
|
||||||
```js
|
|
||||||
myDux.setDefaultMutation( (payload,action) => state => state+1 );
|
|
||||||
```
|
|
@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
title: Effects
|
|
||||||
---
|
|
||||||
|
|
||||||
In addition to mutations, Updux also provides action-specific middleware, here
|
|
||||||
called effects.
|
|
||||||
|
|
||||||
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
|
|
||||||
from the api object.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const myEffect = ({ getState, dispatch, actions, selectors })
|
|
||||||
=> (next) => (action) => {
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Assigning effects to an updux is done via `addEffect`, and follows the same
|
|
||||||
pattern as `addMutation`. The filter can be the type of an action,
|
|
||||||
an action creator, a guard function, or nothing at all.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const myEffect = ({getState,dispatch}) => (next) => (action) => {
|
|
||||||
|
|
||||||
next(action);
|
|
||||||
|
|
||||||
if( getState.getSpecialThing() )
|
|
||||||
dispatch.doTheOtherThing();
|
|
||||||
};
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addEffect( addTodo, myEffect );
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addEffect( 'addTodo', myEffect );
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addEffect(
|
|
||||||
(action) => action.type.startsWidth('todo'),
|
|
||||||
myEffect
|
|
||||||
);
|
|
||||||
|
|
||||||
// valid
|
|
||||||
myDux.addEffect( myEffect );
|
|
||||||
``````
|
|
@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
title: Subduxes
|
|
||||||
---
|
|
||||||
|
|
||||||
Subduxes are how Updux goes recursive. They are upduxes (or updux
|
|
||||||
configurations) themselves and are provided at contruction time.
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
const bar = ;
|
|
||||||
|
|
||||||
const mainDux = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
bar: new Updux({
|
|
||||||
initialState: {
|
|
||||||
a: 1
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
baz: {
|
|
||||||
initialState: {
|
|
||||||
b: 'two'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
All properties of the subduxes will be incorporated in the main updux.
|
|
@ -1,242 +0,0 @@
|
|||||||
---
|
|
||||||
title: What's Updux?
|
|
||||||
description: Updux manual
|
|
||||||
# template: splash
|
|
||||||
hero:
|
|
||||||
# tagline: Congrats on setting up a new Starlight project!
|
|
||||||
image:
|
|
||||||
file: ../../../public/updux-logo.svg
|
|
||||||
# actions:
|
|
||||||
# - text: Example Guide
|
|
||||||
# link: /guides/example/
|
|
||||||
# icon: right-arrow
|
|
||||||
# variant: primary
|
|
||||||
# - text: Read the Starlight docs
|
|
||||||
# link: https://starlight.astro.build
|
|
||||||
# icon: external
|
|
||||||
---
|
|
||||||
|
|
||||||
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
|
|
||||||
[rematch](https://rematch.github.io/rematch) alonside a few other frameworks built atop Redux.
|
|
||||||
|
|
||||||
It has a couple of pretty good ideas that removes some of the
|
|
||||||
boilerplate. 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.
|
|
||||||
|
|
||||||
All that to say, say hello to `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
|
|
||||||
effects (middleware reacting to actions that can be asynchronous and/or
|
|
||||||
have side-effects), so everything pertaining to a store are all defined
|
|
||||||
in the space place.
|
|
||||||
- Automatically gather all actions used by the updux's effects and mutations,
|
|
||||||
and makes then accessible as attributes to the `dispatch` object of the
|
|
||||||
store.
|
|
||||||
- Mutations have a signature that is friendly to Updux and Immer.
|
|
||||||
- Also, the mutation signature auto-unwrap the payload of the actions for you.
|
|
||||||
- TypeScript types.
|
|
||||||
|
|
||||||
Fair warning: this package is still very new, probably very buggy,
|
|
||||||
definitively very badly documented, and very subject to changes. Caveat
|
|
||||||
Maxima Emptor.
|
|
||||||
|
|
||||||
## Synopsis
|
|
||||||
|
|
||||||
```
|
|
||||||
import updux from 'updux';
|
|
||||||
|
|
||||||
import otherUpdux from './otherUpdux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
initial,
|
|
||||||
reducer,
|
|
||||||
actions,
|
|
||||||
middleware,
|
|
||||||
createStore,
|
|
||||||
} = new Updux({
|
|
||||||
initial: {
|
|
||||||
counter: 0,
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
otherUpdux,
|
|
||||||
},
|
|
||||||
mutations: {
|
|
||||||
inc: ( increment = 1 ) => u({counter: s => s + increment })
|
|
||||||
},
|
|
||||||
effects: {
|
|
||||||
'*' => api => next => action => {
|
|
||||||
console.log( "hey, look, an action zoomed by!", action );
|
|
||||||
next(action);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
customAction: ( someArg ) => ({
|
|
||||||
type: "custom",
|
|
||||||
payload: { someProp: someArg }
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = createStore();
|
|
||||||
|
|
||||||
store.dispatch.inc(3);
|
|
||||||
```
|
|
||||||
|
|
||||||
# Description
|
|
||||||
|
|
||||||
Full documentation can be [found here](https://yanick.github.io/updux/).
|
|
||||||
Right now the best way to understand the whole thing is to go
|
|
||||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
|
||||||
|
|
||||||
## 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 Updux instance as the default export:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
|
|
||||||
const updux = new Updux({ ... });
|
|
||||||
|
|
||||||
export default updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
Then you can use them as subduxes like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
import Updux from 'updux';
|
|
||||||
import foo from './foo'; // foo is an Updux
|
|
||||||
import bar from './bar'; // bar is an Updux as well
|
|
||||||
|
|
||||||
const updux = new Updux({
|
|
||||||
subduxes: {
|
|
||||||
foo, bar
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if you want to use it:
|
|
||||||
|
|
||||||
```
|
|
||||||
import updux from './myUpdux';
|
|
||||||
|
|
||||||
const {
|
|
||||||
reducer,
|
|
||||||
actions: { doTheThing },
|
|
||||||
createStore,
|
|
||||||
middleware,
|
|
||||||
} = updux;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mapping a mutation to all values of a state
|
|
||||||
|
|
||||||
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
|
||||||
enough to have the main reducer maps away all items to the sub-reducer:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({ initial: [] });
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.review,
|
|
||||||
(_,action) => state => state.map( todo.upreducer(action) )
|
|
||||||
);
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
But `updeep` can iterate through all the items of an array (or the values of
|
|
||||||
an object) via the special key `*`. So the todos updux above could also be
|
|
||||||
written:
|
|
||||||
|
|
||||||
```
|
|
||||||
const todo = new Updux({
|
|
||||||
mutations: {
|
|
||||||
review: () => u({ reviewed: true}),
|
|
||||||
done: () => u({done: true}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const todos = new Updux({
|
|
||||||
subduxes: { '*': todo },
|
|
||||||
});
|
|
||||||
|
|
||||||
todos.addMutation(
|
|
||||||
todo.actions.done,
|
|
||||||
(id,action) => u.map(u.if(u.is('id',id), todo.upreducer(action))),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
The advantages being that the actions/mutations/effects of the subdux will be
|
|
||||||
imported by the root updux as usual, and all actions that aren't being
|
|
||||||
overridden by a sink mutation will trickle down 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: 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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
@ -1,11 +0,0 @@
|
|||||||
---
|
|
||||||
title: Example Reference
|
|
||||||
description: A reference page in my new Starlight docs site.
|
|
||||||
---
|
|
||||||
|
|
||||||
Reference pages are ideal for outlining how things work in terse and clear terms.
|
|
||||||
Less concerned with telling a story or addressing a specific use case, they should give a comprehensive outline of what you're documenting.
|
|
||||||
|
|
||||||
## Further reading
|
|
||||||
|
|
||||||
- Read [about reference](https://diataxis.fr/reference/) in the Diátaxis framework
|
|
@ -1,74 +0,0 @@
|
|||||||
---
|
|
||||||
title: Actions
|
|
||||||
sidebar:
|
|
||||||
order: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Code } from '@astrojs/starlight/components';
|
|
||||||
import snippet from './code/actions.test.ts?raw';
|
|
||||||
import extractSnippet from './extractSnippet.js';
|
|
||||||
|
|
||||||
Having a state is good and all of that, but it's also very static.
|
|
||||||
So let's introduce some reducing. First step: let's create some actions.
|
|
||||||
|
|
||||||
<Code code={extractSnippet('actions1')(snippet)} lang="js" />
|
|
||||||
|
|
||||||
`createAction` is actually provided by
|
|
||||||
[@reduxjs/toolkit](https://redux-toolkit.js.org/), we only re-export it
|
|
||||||
for convenience. `withPayload` is an utility function that makes defining the
|
|
||||||
action payload just a little more concise.
|
|
||||||
|
|
||||||
```js
|
|
||||||
createAction('foo', withPayload<string>() );
|
|
||||||
// equivalent to
|
|
||||||
createAction('foo', (payload: string) => ({ payload }) ) );
|
|
||||||
|
|
||||||
createAction('foo', withPayload( (level:number) => ({ level }) );
|
|
||||||
// equivalent to
|
|
||||||
createAction('foo', (level: number) => ({ payload: { level } }) ) );
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding actions to an Updux object
|
|
||||||
|
|
||||||
Actions can be tied to an Updux object both at construction time
|
|
||||||
|
|
||||||
```js
|
|
||||||
const myAction = createAction('myAction');
|
|
||||||
const myOtherAction = createAction('myOtherAction');
|
|
||||||
|
|
||||||
const myDux = new Updux({
|
|
||||||
actions: {
|
|
||||||
myAction,
|
|
||||||
myOtherAction,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
or they will be automatically added when used in methods like `addMutation`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
|
|
||||||
const myDux = new Updux({})
|
|
||||||
.addMutation( myAction, (payload) => state => state + payload );
|
|
||||||
```
|
|
||||||
|
|
||||||
Note the chained invocation of `addMutation`, which is needed if you want
|
|
||||||
the type of myDux to include the `myAction` action.
|
|
||||||
|
|
||||||
```js
|
|
||||||
|
|
||||||
// myDux will have the right type for myDux.actions.myAction
|
|
||||||
myDux = new Updux({})
|
|
||||||
.addMutation( myAction, (payload) => state => state + payload );
|
|
||||||
|
|
||||||
// myDux type will not know about myAction.
|
|
||||||
myDux = new Updux({});
|
|
||||||
myDux.addMutation( myAction, (payload) => state => state + payload );
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accessing Updux actions
|
|
||||||
|
|
||||||
Once an action is defined, its creator is accessible via the `actions` accessor.
|
|
||||||
|
|
||||||
<Code code={extractSnippet('actions2')(snippet)} lang="js" />
|
|
@ -1 +0,0 @@
|
|||||||
../../../../../src/tutorial
|
|
@ -1,14 +0,0 @@
|
|||||||
export default (fence) => (snippet) => {
|
|
||||||
snippet = snippet.split("\n");
|
|
||||||
snippet = snippet.slice(snippet.findIndex(l => l.includes(fence)) + 1);
|
|
||||||
snippet = snippet.slice(0, snippet.findIndex(l => l.includes(fence)));
|
|
||||||
|
|
||||||
const indent = Math.min(
|
|
||||||
...snippet.map(l => l === '' ? 999 : l.match(/^ */)[0].length)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (indent)
|
|
||||||
snippet = snippet.map(l => l.slice(indent));
|
|
||||||
|
|
||||||
return snippet.join("\n");
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
title: State definition
|
|
||||||
sidebar:
|
|
||||||
order: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Code } from '@astrojs/starlight/components';
|
|
||||||
import snippet from './code/initialState.test.ts?raw';
|
|
||||||
import extractSnippet from './extractSnippet.js';
|
|
||||||
|
|
||||||
Let's start slow with an Updux document that has nothing but an initial
|
|
||||||
state.
|
|
||||||
|
|
||||||
<Code code={extractSnippet('tut1')(snippet)} lang="js" />
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
<Code code={extractSnippet('tut2')(snippet)} lang="js" />
|
|
@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: Introduction
|
|
||||||
description: Introducing the tutorial
|
|
||||||
sidebar:
|
|
||||||
order: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
This tutorial walks you through the features of `Updux` using the
|
|
||||||
time-honored example of the implementation of Todo list store.
|
|
||||||
|
|
||||||
For simplicity sake, we'll write our reducer code using plain JavaScript.
|
|
||||||
But for real-life applications where we want some help with
|
|
||||||
immutability and deep merging, Updux plays extremely well with libraries and
|
|
||||||
tools like [Mutative], [immer][], [updeep][],
|
|
||||||
[remeda][],
|
|
||||||
[lodash][], etc.
|
|
||||||
|
|
||||||
[updeep]: https://www.npmjs.com/package/@yanick/updeep-remeda
|
|
||||||
[immer]: https://www.npmjs.com/package/immer
|
|
||||||
[lodash]: https://www.npmjs.com/package/lodash
|
|
||||||
[ts-action]: https://www.npmjs.com/package/ts-action
|
|
||||||
[remeda]: remedajs.com/
|
|
||||||
[Mutative]: https://mutative.js.org/
|
|
@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
title: Mutations
|
|
||||||
sidebar:
|
|
||||||
order: 4
|
|
||||||
---
|
|
||||||
|
|
||||||
import { Code } from '@astrojs/starlight/components';
|
|
||||||
import snippet from './code/actions.test.ts?raw';
|
|
||||||
import extractSnippet from './extractSnippet.js';
|
|
||||||
|
|
||||||
Mutations are what tie actions to a reducing function.
|
|
||||||
|
|
||||||
## Defining a mutation
|
|
||||||
|
|
||||||
The signature of a mutation is just a smidge different than the typical
|
|
||||||
Redux reducing functions:
|
|
||||||
|
|
||||||
```js
|
|
||||||
( actionPayload, action ) => state => newState
|
|
||||||
```
|
|
||||||
|
|
||||||
They are tied to actions in an updux via the method `addMutation`.
|
|
||||||
|
|
||||||
<Code code={extractSnippet('addMutation-1')(snippet)} lang="js" />
|
|
2
docs/src/env.d.ts
vendored
2
docs/src/env.d.ts
vendored
@ -1,2 +0,0 @@
|
|||||||
/// <reference path="../.astro/types.d.ts" />
|
|
||||||
/// <reference types="astro/client" />
|
|
@ -1,10 +1,10 @@
|
|||||||
import Updux from '../index.js';
|
import Updux from '../src/index.js';
|
||||||
import u from '@yanick/updeep-remeda';
|
import u from '@yanick/updeep-remeda';
|
||||||
|
|
||||||
export default new Updux({
|
const todoDux = new Updux({
|
||||||
initialState: {
|
initialState: {
|
||||||
id: 0,
|
id: 0,
|
||||||
description: '',
|
description: "",
|
||||||
done: false,
|
done: false,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -12,7 +12,9 @@ export default new Updux({
|
|||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
desc: ({ description }) => description,
|
desc: ({ description }) => description,
|
||||||
},
|
}
|
||||||
})
|
});
|
||||||
.addMutation('todoDone', () => u({ done: true }));
|
|
||||||
|
|
||||||
|
todoDux.addMutation('todoDone', () => u({ done: true }));
|
||||||
|
|
||||||
|
export default todoDux.asDux;
|
28
docs/todoList.ts
Normal file
28
docs/todoList.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Updux from '../src/index.js';
|
||||||
|
import nextIdDux from './nextId';
|
||||||
|
import todosDux from './todos.js';
|
||||||
|
|
||||||
|
const todosListDux = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
todos: todosDux,
|
||||||
|
nextId: nextIdDux,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addTodo: (description: string) => description,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
todosListDux.addEffect(
|
||||||
|
'addTodo', ({ getState, dispatch }) => next => action => {
|
||||||
|
const id = getState.getNextId();
|
||||||
|
|
||||||
|
dispatch.incNextId();
|
||||||
|
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
dispatch.addTodoWithId(action.payload, id);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
export default todosListDux;
|
26
docs/todos.ts
Normal file
26
docs/todos.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import Updux from '../src/index.js';
|
||||||
|
import u from '@yanick/updeep-remeda';
|
||||||
|
|
||||||
|
import todoDux from './todo.js';
|
||||||
|
|
||||||
|
const todosDux = new Updux({
|
||||||
|
initialState: [] as (typeof todoDux.initialState)[],
|
||||||
|
actions: {
|
||||||
|
addTodoWithId: (description, id) => ({ description, id }),
|
||||||
|
todoDone: (id: number) => (id),
|
||||||
|
},
|
||||||
|
findSelectors: {
|
||||||
|
getTodoById: state => id => state.find(u.matches({ id }))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
todosDux.addMutation('addTodoWithId', todo => todos => todos.concat({ ...todo, done: false }));
|
||||||
|
|
||||||
|
todosDux.addMutation(
|
||||||
|
'todoDone', (id, action) => u.map(
|
||||||
|
u.if(u.matches({ id }), todoDux.upreducer(action))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
export default todosDux.asDux;
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "astro/tsconfigs/strict",
|
|
||||||
"compilerOptions": {
|
|
||||||
"moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
|
||||||
"paths": {
|
|
||||||
"updux": ["../src/index.ts"],
|
|
||||||
"@tutorial/*": [ "../src/tutorial/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
19
docs/tutorial-1.test.js
Normal file
19
docs/tutorial-1.test.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
/// [tut1]
|
||||||
|
import Updux from 'updux';
|
||||||
|
const todosDux = new Updux({
|
||||||
|
initialState: {
|
||||||
|
nextId: 1,
|
||||||
|
todos: [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/// [tut1]
|
||||||
|
/// [tut2]
|
||||||
|
const store = todosDux.createStore();
|
||||||
|
store.getState(); // { nextId: 1, todos: [] }
|
||||||
|
/// [tut2]
|
||||||
|
test("basic", () => {
|
||||||
|
expect(store.getState()).toEqual({
|
||||||
|
nextId: 1, todos: []
|
||||||
|
});
|
||||||
|
});
|
25
docs/tutorial-1.test.ts
Normal file
25
docs/tutorial-1.test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
|
/// [tut1]
|
||||||
|
import Updux from 'updux';
|
||||||
|
|
||||||
|
const todosDux = new Updux({
|
||||||
|
initialState: {
|
||||||
|
nextId: 1,
|
||||||
|
todos: [],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// [tut1]
|
||||||
|
|
||||||
|
/// [tut2]
|
||||||
|
const store = todosDux.createStore();
|
||||||
|
|
||||||
|
store.getState(); // { nextId: 1, todos: [] }
|
||||||
|
/// [tut2]
|
||||||
|
|
||||||
|
test("basic", () => {
|
||||||
|
expect(store.getState()).toEqual({
|
||||||
|
nextId: 1, todos: []
|
||||||
|
})
|
||||||
|
});
|
56
docs/tutorial-actions.test.js
Normal file
56
docs/tutorial-actions.test.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
/// [actions1]
|
||||||
|
import Updux, { createAction, withPayload } from 'updux';
|
||||||
|
const addTodo = createAction('addTodo', withPayload());
|
||||||
|
const todoDone = createAction('todoDone', withPayload());
|
||||||
|
const todosDux = new Updux({
|
||||||
|
initialState: {
|
||||||
|
nextId: 1,
|
||||||
|
todos: [],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addTodo,
|
||||||
|
todoDone,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/// [actions1]
|
||||||
|
/// [actions2]
|
||||||
|
todosDux.actions.addTodo('write tutorial');
|
||||||
|
// { type: 'addTodo', payload: 'write tutorial' }
|
||||||
|
/// [actions2]
|
||||||
|
test("basic", () => {
|
||||||
|
expect(todosDux.actions.addTodo('write tutorial')).toEqual({
|
||||||
|
type: 'addTodo', payload: 'write tutorial'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/// [addMutation]
|
||||||
|
todosDux.addMutation(addTodo, (description) => (state) => {
|
||||||
|
state.todos.unshift({ description, id: state.nextId, done: false });
|
||||||
|
state.nextId++;
|
||||||
|
});
|
||||||
|
const store = todosDux.createStore();
|
||||||
|
store.dispatch.addTodo('write tutorial');
|
||||||
|
const state = store.getState();
|
||||||
|
// {
|
||||||
|
// nextId: 2,
|
||||||
|
// todos: [
|
||||||
|
// {
|
||||||
|
// description: 'write tutorial',
|
||||||
|
// done: false,
|
||||||
|
// id: 1,
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
/// [addMutation]
|
||||||
|
test("addMutation", () => {
|
||||||
|
expect(state).toEqual({
|
||||||
|
nextId: 2,
|
||||||
|
todos: [
|
||||||
|
{
|
||||||
|
description: 'write tutorial',
|
||||||
|
done: false,
|
||||||
|
id: 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,6 @@
|
|||||||
import { test, expect } from 'vitest';
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
/// --8<-- [start:actions1]
|
/// [actions1]
|
||||||
import Updux, { createAction, withPayload } from 'updux';
|
import Updux, { createAction, withPayload } from 'updux';
|
||||||
|
|
||||||
type TodoId = number;
|
type TodoId = number;
|
||||||
@ -8,13 +8,6 @@ type TodoId = number;
|
|||||||
const addTodo = createAction('addTodo', withPayload<string>());
|
const addTodo = createAction('addTodo', withPayload<string>());
|
||||||
const todoDone = createAction('todoDone', withPayload<TodoId>());
|
const todoDone = createAction('todoDone', withPayload<TodoId>());
|
||||||
|
|
||||||
/// --8<-- [end:actions1]
|
|
||||||
|
|
||||||
test('createAction', () => {
|
|
||||||
expect(addTodo).toBeTypeOf('function');
|
|
||||||
expect(todoDone).toBeTypeOf('function');
|
|
||||||
});
|
|
||||||
|
|
||||||
const todosDux = new Updux({
|
const todosDux = new Updux({
|
||||||
initialState: {
|
initialState: {
|
||||||
nextId: 1,
|
nextId: 1,
|
||||||
@ -23,39 +16,28 @@ const todosDux = new Updux({
|
|||||||
actions: {
|
actions: {
|
||||||
addTodo,
|
addTodo,
|
||||||
todoDone,
|
todoDone,
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
/// [actions1]
|
||||||
|
|
||||||
/// --8<-- [start:actions2]
|
/// [actions2]
|
||||||
todosDux.actions.addTodo('write tutorial');
|
todosDux.actions.addTodo('write tutorial');
|
||||||
// { type: 'addTodo', payload: 'write tutorial' }
|
// { type: 'addTodo', payload: 'write tutorial' }
|
||||||
/// --8<-- [end:actions2]
|
/// [actions2]
|
||||||
|
|
||||||
test('basic', () => {
|
test("basic", () => {
|
||||||
expect(todosDux.actions.addTodo('write tutorial')).toEqual({
|
expect(todosDux.actions.addTodo('write tutorial')).toEqual({
|
||||||
type: 'addTodo',
|
type: 'addTodo', payload: 'write tutorial'
|
||||||
payload: 'write tutorial',
|
})
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// --8<-- [start:addMutation-1]
|
/// [addMutation]
|
||||||
|
todosDux.addMutation(addTodo, (description) => ({ todos, nextId }) => {
|
||||||
todosDux.addMutation(addTodo, (description) => ({ todos, nextId }) => ({
|
return {
|
||||||
nextId: 1 + nextId,
|
todos: todos.concat({ description, id: nextId, done: false }),
|
||||||
todos: todos.concat({ description, id: nextId, done: false }),
|
nextId: 1 + nextId,
|
||||||
}));
|
}
|
||||||
|
});
|
||||||
todosDux.addMutation(todoDone, (id) => ({ todos, nextId }) => ({
|
|
||||||
nextId: 1 + nextId,
|
|
||||||
todos: todos.map((todo) => {
|
|
||||||
if (todo.id !== id) return todo;
|
|
||||||
return {
|
|
||||||
...todo,
|
|
||||||
done: true,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
/// --8<-- [end:addMutation-1]
|
|
||||||
|
|
||||||
const store = todosDux.createStore();
|
const store = todosDux.createStore();
|
||||||
|
|
||||||
@ -73,9 +55,9 @@ const state = store.getState();
|
|||||||
// ]
|
// ]
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/// --8<-- [end:addMutation]
|
/// [addMutation]
|
||||||
|
|
||||||
test('addMutation', () => {
|
test("addMutation", () => {
|
||||||
expect(state).toEqual({
|
expect(state).toEqual({
|
||||||
nextId: 2,
|
nextId: 2,
|
||||||
todos: [
|
todos: [
|
||||||
@ -83,7 +65,7 @@ test('addMutation', () => {
|
|||||||
description: 'write tutorial',
|
description: 'write tutorial',
|
||||||
done: false,
|
done: false,
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
});
|
});
|
37
docs/tutorial-effects.test.js
Normal file
37
docs/tutorial-effects.test.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
process.env.UPDEEP_MODE = "dangerously_never_freeze";
|
||||||
|
// [effects-1]
|
||||||
|
import u from '@yanick/updeep-remeda';
|
||||||
|
import * as R from 'remeda';
|
||||||
|
import Updux, { createAction, withPayload } from 'updux';
|
||||||
|
const addTodoWithId = createAction('addTodoWithId', withPayload());
|
||||||
|
const incNextId = createAction('incNextId');
|
||||||
|
const addTodo = createAction('addTodo', withPayload());
|
||||||
|
const todosDux = new Updux({
|
||||||
|
initialState: { nextId: 1, todos: [] },
|
||||||
|
actions: { addTodo, incNextId, addTodoWithId },
|
||||||
|
selectors: {
|
||||||
|
nextId: ({ nextId }) => nextId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
todosDux.addMutation(addTodoWithId, (todo) => state => state
|
||||||
|
// u({
|
||||||
|
// todos: R.concat([u(todo, { done: false })])
|
||||||
|
// })
|
||||||
|
);
|
||||||
|
todosDux.addMutation(incNextId, () => state => state); //u({ nextId: id => id + 1 }));
|
||||||
|
// todosDux.addEffect(addTodo, ({ getState, dispatch }) => next => action => {
|
||||||
|
// const id = getState.nextId();
|
||||||
|
// dispatch.incNextId();
|
||||||
|
// next(action);
|
||||||
|
// dispatch.addTodoWithId({ id, description: action.payload });
|
||||||
|
// });
|
||||||
|
const store = todosDux.createStore();
|
||||||
|
store.dispatch.addTodo('write tutorial');
|
||||||
|
// [effects-1]
|
||||||
|
test('basic', () => {
|
||||||
|
expect(store.getState()).toMatchObject({
|
||||||
|
nextId: 2,
|
||||||
|
todos: [{ id: 1, description: 'write tutorial', done: false }]
|
||||||
|
});
|
||||||
|
});
|
@ -1,23 +1,21 @@
|
|||||||
import { test, expect } from 'vitest';
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
/// --8<-- [start:effects-1]
|
//process.env.UPDEEP_MODE = "dangerously_never_freeze";
|
||||||
|
|
||||||
|
/// [effects-1]
|
||||||
import u from '@yanick/updeep-remeda';
|
import u from '@yanick/updeep-remeda';
|
||||||
import * as R from 'remeda';
|
import * as R from 'remeda';
|
||||||
|
|
||||||
import Updux, { createAction, withPayload } from 'updux';
|
import Updux, { createAction, withPayload } from '../src/index.js';
|
||||||
|
|
||||||
// we want to decouple the increment of next_id and the creation of
|
// 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 'addTodo'.
|
// a new todo. So let's use a new version of the action 'addTodo'.
|
||||||
|
|
||||||
type TodoId = number;
|
type TodoId = number;
|
||||||
|
|
||||||
const addTodoWithId = createAction(
|
const addTodoWithId = createAction('addTodoWithId', withPayload<{
|
||||||
'addTodoWithId',
|
id: TodoId, description: string
|
||||||
withPayload<{
|
}>());
|
||||||
id: TodoId;
|
|
||||||
description: string;
|
|
||||||
}>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const incNextId = createAction('incNextId');
|
const incNextId = createAction('incNextId');
|
||||||
const addTodo = createAction('addTodo', withPayload<string>());
|
const addTodo = createAction('addTodo', withPayload<string>());
|
||||||
@ -30,37 +28,36 @@ const todosDux = new Updux({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
todosDux.addMutation(addTodoWithId, (todo) =>
|
todosDux.addMutation(addTodoWithId, (todo) => u({
|
||||||
u({
|
todos: R.concat([u(todo, { done: false })])
|
||||||
todos: R.concat([u(todo, { done: false })]),
|
}));
|
||||||
}),
|
|
||||||
|
todosDux.addMutation(
|
||||||
|
incNextId, () => u({ nextId: id => id + 1 })
|
||||||
);
|
);
|
||||||
|
|
||||||
todosDux.addMutation(incNextId, () => u({ nextId: (id) => id + 1 }));
|
todosDux.addEffect("addTodo", ({ getState, dispatch }) => next => action => {
|
||||||
|
const id = getState.nextId();
|
||||||
|
|
||||||
todosDux.addEffect(
|
dispatch.incNextId();
|
||||||
'addTodo',
|
|
||||||
({ getState, dispatch }) =>
|
|
||||||
(next) =>
|
|
||||||
(action) => {
|
|
||||||
const id = getState.nextId();
|
|
||||||
|
|
||||||
dispatch.incNextId();
|
next(action);
|
||||||
|
|
||||||
next(action);
|
dispatch.addTodoWithId(
|
||||||
|
{ id, description: action.payload }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
dispatch.addTodoWithId({ id, description: action.payload });
|
)
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const store = todosDux.createStore();
|
const store = todosDux.createStore();
|
||||||
|
|
||||||
store.dispatch.addTodo('write tutorial');
|
store.dispatch.addTodo('write tutorial');
|
||||||
/// --8<-- [end:effects-1]
|
/// [effects-1]
|
||||||
|
|
||||||
test('basic', () => {
|
test('basic', () => {
|
||||||
expect(store.getState()).toMatchObject({
|
expect(store.getState()).toMatchObject({
|
||||||
nextId: 2,
|
nextId: 2,
|
||||||
todos: [{ id: 1, description: 'write tutorial', done: false }],
|
todos: [{ id: 1, description: 'write tutorial', done: false }]
|
||||||
});
|
})
|
||||||
});
|
})
|
42
docs/tutorial-final.test.ts
Normal file
42
docs/tutorial-final.test.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { expectTypeOf } from 'expect-type';
|
||||||
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
|
import todoListDux from './todoList.js';
|
||||||
|
|
||||||
|
test("basic", () => {
|
||||||
|
const store = todoListDux.createStore();
|
||||||
|
|
||||||
|
store.dispatch.addTodo('write tutorial');
|
||||||
|
store.dispatch.addTodo('test code snippets');
|
||||||
|
|
||||||
|
store.dispatch.todoDone(2);
|
||||||
|
|
||||||
|
const s = store.getState();
|
||||||
|
|
||||||
|
expectTypeOf(s).toMatchTypeOf<{
|
||||||
|
nextId: number
|
||||||
|
}>();
|
||||||
|
|
||||||
|
expect(store.getState()).toMatchObject({
|
||||||
|
todos: [
|
||||||
|
{ id: 1, done: false },
|
||||||
|
{ id: 2, done: true }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(todoListDux.schema).toMatchObject({
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nextId: { type: 'number', default: 1 },
|
||||||
|
todos: {
|
||||||
|
default: [],
|
||||||
|
type: 'array',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
nextId: 1,
|
||||||
|
todos: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -1,8 +1,8 @@
|
|||||||
import { test, expect } from 'vitest';
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
/// --8<-- [start:mono]
|
/// [mono]
|
||||||
|
|
||||||
import Updux from '../index.js';
|
import Updux from '../src/index.js';
|
||||||
import u from '@yanick/updeep-remeda';
|
import u from '@yanick/updeep-remeda';
|
||||||
|
|
||||||
type TodoId = number;
|
type TodoId = number;
|
||||||
@ -14,30 +14,33 @@ const todosDux = new Updux({
|
|||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
addTodo: (description: string) => description,
|
addTodo: (description: string) => description,
|
||||||
addTodoWithId: (description: string, id: TodoId) => ({
|
addTodoWithId: (description: string, id: TodoId) => ({ description, id, done: false }),
|
||||||
description,
|
|
||||||
id,
|
|
||||||
done: false,
|
|
||||||
}),
|
|
||||||
todoDone: (id: TodoId) => id,
|
todoDone: (id: TodoId) => id,
|
||||||
incNextId: () => { },
|
incNextId: null,
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
getTodoById:
|
getTodoById: ({ todos }) => id => todos.find(u.matches({ id })),
|
||||||
({ todos }) =>
|
|
||||||
(id) =>
|
|
||||||
todos.find(u.matches({ id })),
|
|
||||||
getNextId: ({ nextId }) => nextId,
|
getNextId: ({ nextId }) => nextId,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.addMutation('addTodoWithId', (todo) =>
|
|
||||||
u.updateIn('todos', (todos) => [...todos, todo]),
|
todosDux.addMutation(
|
||||||
|
todosDux.actions.addTodoWithId, todo =>
|
||||||
|
u.updateIn('todos', todos => [...todos, todo]),
|
||||||
|
);
|
||||||
|
|
||||||
|
todosDux.addMutation(
|
||||||
|
todosDux.actions.incNextId,
|
||||||
|
() => u({ nextId: x => x + 1 }));
|
||||||
|
|
||||||
|
todosDux.addMutation(
|
||||||
|
todosDux.actions.todoDone, (id) => u.updateIn('todos',
|
||||||
|
u.map(u.if(u.matches({ id }), { done: true }))
|
||||||
)
|
)
|
||||||
.addMutation('incNextId', () => u({ nextId: (x) => x + 1 }))
|
);
|
||||||
.addMutation('todoDone', (id) =>
|
|
||||||
u.updateIn('todos', u.map(u.if(u.matches({ id }), { done: true }))),
|
todosDux.addEffect(
|
||||||
)
|
todosDux.actions.addTodo, ({ getState, dispatch }) => next => action => {
|
||||||
.addEffect('addTodo', ({ getState, dispatch }) => (next) => (action) => {
|
|
||||||
const id = getState.getNextId();
|
const id = getState.getNextId();
|
||||||
|
|
||||||
dispatch.incNextId();
|
dispatch.incNextId();
|
||||||
@ -45,10 +48,10 @@ const todosDux = new Updux({
|
|||||||
next(action);
|
next(action);
|
||||||
|
|
||||||
dispatch.addTodoWithId(action.payload, id);
|
dispatch.addTodoWithId(action.payload, id);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/// [mono]
|
||||||
/// --8<-- [end:mono]
|
|
||||||
|
|
||||||
test('basic', () => {
|
test('basic', () => {
|
||||||
const store = todosDux.createStore();
|
const store = todosDux.createStore();
|
||||||
@ -59,8 +62,8 @@ test('basic', () => {
|
|||||||
nextId: 3,
|
nextId: 3,
|
||||||
todos: [
|
todos: [
|
||||||
{ id: 1, description: 'write tutorial', done: false },
|
{ id: 1, description: 'write tutorial', done: false },
|
||||||
{ id: 2, description: 'have fun', done: false },
|
{ id: 2, description: 'have fun', done: false }
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
store.dispatch.todoDone(1);
|
store.dispatch.todoDone(1);
|
||||||
@ -70,6 +73,6 @@ test('basic', () => {
|
|||||||
todos: [
|
todos: [
|
||||||
{ id: 1, description: 'write tutorial', done: true },
|
{ id: 1, description: 'write tutorial', done: true },
|
||||||
{ id: 2, description: 'have fun', done: false },
|
{ id: 2, description: 'have fun', done: false },
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
});
|
})
|
39
docs/tutorial-reactions.test.js
Normal file
39
docs/tutorial-reactions.test.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
import u from 'updeep';
|
||||||
|
|
||||||
|
import { Updux } from '../src/index.js';
|
||||||
|
|
||||||
|
const todos = new Updux({
|
||||||
|
initial: [],
|
||||||
|
actions: {
|
||||||
|
setNbrTodos: null,
|
||||||
|
addTodo: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
addTodo: todo => todos => [ ...todos, todo ],
|
||||||
|
},
|
||||||
|
reactions: [
|
||||||
|
({dispatch}) => todos => dispatch.setNbrTodos(todos.length)
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const myDux = new Updux({
|
||||||
|
initial: {
|
||||||
|
nbrTodos: 0
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
todos,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setNbrTodos: nbrTodos => u({ nbrTodos })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test( "basic tests", async () => {
|
||||||
|
const store = myDux.createStore();
|
||||||
|
|
||||||
|
store.dispatch.addTodo('one');
|
||||||
|
store.dispatch.addTodo('two');
|
||||||
|
|
||||||
|
expect(store.getState().nbrTodos).toEqual(2);
|
||||||
|
});
|
27
docs/tutorial-selectors.test.js
Normal file
27
docs/tutorial-selectors.test.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
|
import { Updux } from '../src/index.js';
|
||||||
|
|
||||||
|
test( 'selectors', () => {
|
||||||
|
const dux = new Updux({
|
||||||
|
initial: { a: 1, b: 2 },
|
||||||
|
selectors: {
|
||||||
|
getA: ({a}) => a,
|
||||||
|
getBPlus: ({b}) => addition => b + addition,
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
subbie: new Updux({
|
||||||
|
initial: { d: 3 },
|
||||||
|
selectors: {
|
||||||
|
getD: ({d}) => d
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const store = dux.createStore();
|
||||||
|
|
||||||
|
expect( store.getState.getA() ).toEqual(1);
|
||||||
|
expect( store.getState.getBPlus(7) ).toEqual(9);
|
||||||
|
expect( store.getState.getD() ).toEqual(3);
|
||||||
|
} );
|
@ -1,44 +1,42 @@
|
|||||||
import { test, expect } from 'vitest';
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
/// --8<-- [start:sel1]
|
/// [sel1]
|
||||||
import Updux from 'updux';
|
import Updux from 'updux';
|
||||||
|
|
||||||
const dux = new Updux({
|
const dux = new Updux({
|
||||||
initialState: [] as { id: number; done: boolean }[],
|
initialState: [] as { id: number, done: boolean }[],
|
||||||
selectors: {
|
selectors: {
|
||||||
getDone: (state) => state.filter(({ done }) => done),
|
getDone: (state) => state.filter(({ done }) => done),
|
||||||
getById: (state) => (id) => state.find((todo) => todo.id === id),
|
getById: (state) => (id) => state.find(todo => todo.id === id),
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = [
|
const state = [{ id: 1, done: true }, { id: 2, done: false }];
|
||||||
{ id: 1, done: true },
|
|
||||||
{ id: 2, done: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
dux.selectors.getDone(state); // = [ { id: 1, done: true } ]
|
dux.selectors.getDone(state); // = [ { id: 1, done: true } ]
|
||||||
dux.selectors.getById(state)(2); // = { id: 2, done: false }
|
dux.selectors.getById(state)(2); // = { id: 2, done: false }
|
||||||
|
|
||||||
const store = dux.createStore({ preloadedState: state });
|
const store = dux.createStore({ preloadedState: state });
|
||||||
|
|
||||||
store.selectors.getDone(state); // = [ { id: 1, done: true } ]
|
store.selectors.getDone(state); // = [ { id: 1, done: true } ]
|
||||||
store.selectors.getById(state)(2); // = { id: 2, done: false }
|
store.selectors.getById(state)(2); // = { id: 2, done: false }
|
||||||
|
|
||||||
store.getState.getDone(); // = [ { id: 1, done: true } ]
|
store.getState.getDone(); // = [ { id: 1, done: true } ]
|
||||||
store.getState.getById(2); // = { id: 2, done: false }
|
store.getState.getById(2); // = { id: 2, done: false }
|
||||||
|
|
||||||
/// --8<-- [end:sel1]
|
/// [sel1]
|
||||||
|
|
||||||
test('selectors', () => {
|
test('selectors', () => {
|
||||||
expect(dux.selectors.getDone(state)).toMatchObject([{ id: 1, done: true }]);
|
expect(dux.selectors.getDone(state)).toMatchObject([{ id: 1, done: true }]);
|
||||||
expect(dux.selectors.getById(state)(2)).toMatchObject({ id: 2 });
|
expect(dux.selectors.getById(state)(2)).toMatchObject({ id: 2 });
|
||||||
|
|
||||||
expect(store.selectors.getDone(state)).toMatchObject([
|
expect(store.selectors.getDone(state)).toMatchObject([{ id: 1, done: true }]);
|
||||||
{ id: 1, done: true },
|
|
||||||
]);
|
|
||||||
expect(store.selectors.getById(state)(2)).toMatchObject({ id: 2 });
|
expect(store.selectors.getById(state)(2)).toMatchObject({ id: 2 });
|
||||||
|
|
||||||
expect(store.getState()).toMatchObject([{ id: 1 }, { id: 2 }]);
|
expect(store.getState()).toMatchObject([
|
||||||
|
{ id: 1 }, { id: 2 }
|
||||||
|
]);
|
||||||
expect(store.getState.getDone()).toMatchObject([{ id: 1, done: true }]);
|
expect(store.getState.getDone()).toMatchObject([{ id: 1, done: true }]);
|
||||||
expect(store.getState.getById(2)).toMatchObject({ id: 2 });
|
expect(store.getState.getById(2)).toMatchObject({ id: 2 });
|
||||||
|
|
||||||
});
|
});
|
91
docs/tutorial-subduxes.test.js
Normal file
91
docs/tutorial-subduxes.test.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
|
import u from 'updeep';
|
||||||
|
import R from 'remeda';
|
||||||
|
|
||||||
|
import { Updux } from '../src/index.js';
|
||||||
|
|
||||||
|
const nextIdDux = new Updux({
|
||||||
|
initial: 1,
|
||||||
|
actions: {
|
||||||
|
incrementNextId: null,
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
getNextId: state => state
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
incrementNextId: () => state => state + 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const matches = conditions => target => Object.entries(conditions).every(
|
||||||
|
([key,value]) => typeof value === 'function' ? value(target[key]) : target[key] === value
|
||||||
|
);
|
||||||
|
|
||||||
|
const todoDux = new Updux({
|
||||||
|
initial: {
|
||||||
|
id: 0,
|
||||||
|
description: "",
|
||||||
|
done: false,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
todoDone: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
todoDone: id => u.if( matches({id}), { done: true })
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
desc: R.prop('description'),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const todosDux = new Updux({
|
||||||
|
initial: [],
|
||||||
|
subduxes: {
|
||||||
|
'*': todoDux
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addTodoWithId: (description, id) => ({description, id} )
|
||||||
|
},
|
||||||
|
findSelectors: {
|
||||||
|
getTodoById: state => id => state.find(matches({id}))
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
addTodoWithId: todo => todos => [...todos, todo]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mainDux = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
nextId: nextIdDux,
|
||||||
|
todos: todosDux,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addTodo: null
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
addTodo: ({ getState, dispatch }) => next => action => {
|
||||||
|
const id = getState.getNextId();
|
||||||
|
|
||||||
|
dispatch.incrementNextId()
|
||||||
|
|
||||||
|
next(action);
|
||||||
|
|
||||||
|
dispatch.addTodoWithId( action.payload, id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = mainDux.createStore();
|
||||||
|
|
||||||
|
|
||||||
|
test( "basic tests", () => {
|
||||||
|
|
||||||
|
const myDesc = 'do the thing';
|
||||||
|
store.dispatch.addTodo(myDesc);
|
||||||
|
|
||||||
|
expect( store.getState.getTodoById(1).desc() ).toEqual(myDesc);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
168
docs/tutorial.md
Normal file
168
docs/tutorial.md
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# Tutorial
|
||||||
|
|
||||||
|
This tutorial walks you through the features of `Updux` using the
|
||||||
|
time-honored example of the implementation of Todo list store.
|
||||||
|
|
||||||
|
We'll be using
|
||||||
|
[@yanick/updeep-remeda](https://www.npmjs.com/package/@yanick/updeep-remeda) 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][],
|
||||||
|
[remeda][],
|
||||||
|
[lodash][], or even
|
||||||
|
plain JavaScript.
|
||||||
|
|
||||||
|
## Definition of the state
|
||||||
|
|
||||||
|
To begin with, let's define that has nothing but an initial state.
|
||||||
|
|
||||||
|
[filename](tutorial-1.test.ts ':include :type=code :fragment=tut1')
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
[filename](tutorial-1.test.ts ':include :type=code :fragment=tut2')
|
||||||
|
|
||||||
|
## Add actions
|
||||||
|
|
||||||
|
This is all good, but very static. Let's add some actions!
|
||||||
|
|
||||||
|
|
||||||
|
[filename](tutorial-actions.test.ts ':include :type=code :fragment=actions1')
|
||||||
|
|
||||||
|
### Accessing actions
|
||||||
|
|
||||||
|
Once an action is defined, its creator is accessible via the `actions` accessor.
|
||||||
|
This is not yet terribly exciting, but it'll get more interesting once we begin using
|
||||||
|
subduxes.
|
||||||
|
|
||||||
|
[filename](tutorial-actions.test.ts ':include :type=code :fragment=actions2')
|
||||||
|
|
||||||
|
### Adding a mutation
|
||||||
|
|
||||||
|
Mutations are the reducing functions associated to actions. They
|
||||||
|
are defined via the `addMutation` method.
|
||||||
|
|
||||||
|
[filename](tutorial-actions.test.ts ':include :type=code :fragment=addMutation')
|
||||||
|
|
||||||
|
|
||||||
|
Note that in the mutation we take the liberty of changing the state directly.
|
||||||
|
Typically, that'd be a big no-no, but we're safe here because updux wraps all mutations in an immer `produce`.
|
||||||
|
|
||||||
|
|
||||||
|
## Effects
|
||||||
|
|
||||||
|
In addition to mutations, Updux also provides action-specific middleware, here
|
||||||
|
called effects.
|
||||||
|
|
||||||
|
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
|
||||||
|
from the api object.
|
||||||
|
|
||||||
|
[filename](tutorial-effects.test.ts ':include :type=code :fragment=effects-1')
|
||||||
|
|
||||||
|
## Selectors
|
||||||
|
|
||||||
|
Selectors can be defined to get data derived from the state.
|
||||||
|
The `getState` method of a dux store will be augmented
|
||||||
|
with its selectors, with the first call for the state already
|
||||||
|
curried for you.
|
||||||
|
|
||||||
|
[filename](tutorial-selectors.test.ts ':include :type=code :fragment=sel1')
|
||||||
|
|
||||||
|
## Subduxes
|
||||||
|
|
||||||
|
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 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 dux we have so far:
|
||||||
|
|
||||||
|
[filename](tutorial-monolith.test.ts ':include :type=code :fragment=mono')
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### NextId dux
|
||||||
|
|
||||||
|
[filename](nextId.ts ':include :type=code :fragment=dux')
|
||||||
|
|
||||||
|
### Todo updux
|
||||||
|
|
||||||
|
[filename](todo.ts ':include :type=code')
|
||||||
|
|
||||||
|
### Todos updux
|
||||||
|
|
||||||
|
[filename](todos.ts ':include :type=code')
|
||||||
|
|
||||||
|
### Main store
|
||||||
|
|
||||||
|
[filename](todoList.ts ':include :type=code')
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|
```
|
||||||
|
(storeApi) => (state, previousState, unsubscribe) => {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Subscriptions registered for an updux and its subduxes are automatically
|
||||||
|
subscribed to the store when calling `createStore`.
|
||||||
|
|
||||||
|
The `state` passed to the subscriptions of the subduxes is the local state.
|
||||||
|
|
||||||
|
Also, all subscriptions are wrapped such that they are called only if the
|
||||||
|
local `state` changed since their last invocation.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
const todos = new Updux({
|
||||||
|
initial: [],
|
||||||
|
actions: {
|
||||||
|
setNbrTodos: null,
|
||||||
|
addTodo: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
addTodo: todo => todos => [ ...todos, todo ],
|
||||||
|
},
|
||||||
|
reactions: [
|
||||||
|
({dispatch}) => todos => dispatch.setNbrTodos(todos.length)
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const myDux = new Updux({
|
||||||
|
initial: {
|
||||||
|
nbrTodos: 0
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
todos,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setNbrTodos: nbrTodos => u({ nbrTodos })
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
[immer]: https://www.npmjs.com/package/immer
|
||||||
|
[lodash]: https://www.npmjs.com/package/lodash
|
||||||
|
[ts-action]: https://www.npmjs.com/package/ts-action
|
||||||
|
[remeda]: remedajs.com/
|
26
docs/tutorial.test.js
Normal file
26
docs/tutorial.test.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
|
||||||
|
import { Updux } from '../src/index.js';
|
||||||
|
|
||||||
|
test( "basic checks", async () => {
|
||||||
|
|
||||||
|
|
||||||
|
const todosDux = new Updux({
|
||||||
|
initial: {
|
||||||
|
next_id: 1,
|
||||||
|
todos: [],
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addTodo: null,
|
||||||
|
todoDone: null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = todosDux.createStore();
|
||||||
|
|
||||||
|
expect(store.getState()).toEqual({ next_id: 1, todos: [] });
|
||||||
|
|
||||||
|
expect(store.actions.addTodo("learn updux")).toMatchObject({
|
||||||
|
type: 'addTodo', payload: 'learn updux'
|
||||||
|
})
|
||||||
|
});
|
@ -1,8 +0,0 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
|
|
||||||
export const prerender = true;
|
|
||||||
|
|
||||||
/** @type {import('./$types').PageLoad} */
|
|
||||||
export function load() {
|
|
||||||
throw redirect(307, '/docs/first-category/first-page');
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
title: Introduction
|
|
||||||
---
|
|
||||||
|
|
||||||
Updux is a class that collects together all the stuff pertaining to a Redux
|
|
||||||
reducer -- actions, middleware, subscriptions, selectors -- in a way that
|
|
||||||
is as modular and as TypeScript-friendly as possible.
|
|
||||||
|
|
||||||
While it has originally been created to play well with [updeep][], it also
|
|
||||||
work wells with plain JavaScript, and
|
|
||||||
interfaces very neatly with other immutability/deep merging libraries
|
|
||||||
like
|
|
||||||
[Mutative], [immer][], [updeep][],
|
|
||||||
[remeda][],
|
|
||||||
[lodash][], etc.
|
|
||||||
|
|
||||||
## Updux terminology
|
|
||||||
|
|
||||||
<dl>
|
|
||||||
<dt>Updux</dt>
|
|
||||||
<dd>Object encapsulating the information pertinent for a Redux reducer.
|
|
||||||
Named thus because it has been designed to work well with [updeep][],
|
|
||||||
and follows my spin on
|
|
||||||
the [Ducks pattern](https://github.com/erikras/ducks-modular-redux).</dd>
|
|
||||||
<dt>Mutation</dt>
|
|
||||||
<dd>Reducing function, mostly associated with an action. All mutations of
|
|
||||||
an updux object are combined to form its reducer.</dd>
|
|
||||||
<dt>Subdux</dt>
|
|
||||||
<dd>Updux objects associated with sub-states of a main updux. The main
|
|
||||||
updux will inherit all of its subduxes actions, mutations, reactions,
|
|
||||||
etc.</dd>
|
|
||||||
<dt>Effect</dt>
|
|
||||||
<dd>A Redux middleware, augmented with a few goodies.</dd>
|
|
||||||
<dt>Reaction</dt>
|
|
||||||
<dd>Subscription to a updux. Unlike regular Redux subscriptions, don't
|
|
||||||
trigger if the state of the updux isn't changed by the reducing.</dd>
|
|
||||||
|
|
||||||
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
|
|
||||||
[updeep]: https://www.npmjs.com/package/@yanick/updeep-remeda
|
|
||||||
[immer]: https://www.npmjs.com/package/immer
|
|
||||||
[lodash]: https://www.npmjs.com/package/lodash
|
|
||||||
[ts-action]: https://www.npmjs.com/package/ts-action
|
|
||||||
[remeda]: remedajs.com/
|
|
||||||
[Mutative]: https://mutative.js.org/
|
|
16
mkdocs.yml
16
mkdocs.yml
@ -1,16 +0,0 @@
|
|||||||
site_name: Updux
|
|
||||||
theme:
|
|
||||||
name: material
|
|
||||||
markdown_extensions:
|
|
||||||
- pymdownx.snippets
|
|
||||||
- pymdownx.inlinehilite
|
|
||||||
- pymdownx.superfences
|
|
||||||
- pymdownx.highlight:
|
|
||||||
default_lang: js
|
|
||||||
anchor_linenums: true
|
|
||||||
line_spans: __span
|
|
||||||
pygments_lang_class: true
|
|
||||||
nav:
|
|
||||||
- Home: index.md
|
|
||||||
- Tutorial: tutorial.md
|
|
||||||
- API: api/modules.md
|
|
115
package.json
115
package.json
@ -1,62 +1,59 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mobily/ts-belt": "^3.13.1",
|
"@yanick/updeep-remeda": "^2.2.0",
|
||||||
"@yanick/updeep-remeda": "^2.3.1",
|
"ajv": "^8.12.0",
|
||||||
"ajv": "^8.12.0",
|
"expect-type": "^0.16.0",
|
||||||
"expect-type": "^0.16.0",
|
"immer": "^9.0.15",
|
||||||
"immer": "^9.0.15",
|
"json-schema-shorthand": "^2.0.0",
|
||||||
"json-schema-shorthand": "^2.0.0",
|
"json-schema-to-ts": "^2.9.2",
|
||||||
"json-schema-to-ts": "^2.9.2",
|
"memoize-one": "^6.0.0",
|
||||||
"memoize-one": "^6.0.0",
|
"moize": "^6.1.6",
|
||||||
"moize": "^6.1.6",
|
"redux": "^4.2.0",
|
||||||
"redux": "^4.2.0",
|
"remeda": "^1.0.1",
|
||||||
"remeda": "^1.0.1",
|
"updeep": "^1.2.1"
|
||||||
"updeep": "^1.2.1"
|
},
|
||||||
},
|
"license": "MIT",
|
||||||
"license": "MIT",
|
"module": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"types": "./dist/index.d.ts",
|
||||||
"types": "./dist/index.d.ts",
|
"exports": {
|
||||||
"exports": {
|
".": {
|
||||||
".": {
|
"import": "./dist/index.js"
|
||||||
"import": "./dist/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "updux",
|
|
||||||
"description": "Updeep-friendly Redux helper framework",
|
|
||||||
"scripts": {
|
|
||||||
"docsify:serve": "docsify serve docs"
|
|
||||||
},
|
|
||||||
"version": "5.1.0",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/yanick/updux.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"redux",
|
|
||||||
"updeep"
|
|
||||||
],
|
|
||||||
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/yanick/updux/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/yanick/updux#readme",
|
|
||||||
"devDependencies": {
|
|
||||||
"@reduxjs/toolkit": "==2.0.0-alpha.5 ",
|
|
||||||
"@vitest/browser": "^0.23.1",
|
|
||||||
"@vitest/ui": "^2.0.5",
|
|
||||||
"docsify": "^4.13.1",
|
|
||||||
"eslint": "^8.22.0",
|
|
||||||
"eslint-plugin-no-only-tests": "^3.0.0",
|
|
||||||
"eslint-plugin-todo-plz": "^1.2.1",
|
|
||||||
"jsdoc-to-markdown": "^7.1.1",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"redux-toolkit": "^1.1.2",
|
|
||||||
"tsdoc-markdown": "^0.0.4",
|
|
||||||
"typedoc": "^0.25.9",
|
|
||||||
"typedoc-plugin-markdown": "^3.17.1",
|
|
||||||
"typescript": "^4.9.5",
|
|
||||||
"vite": "^4.2.1",
|
|
||||||
"vitest": "2.0.4"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"name": "updux",
|
||||||
|
"description": "Updeep-friendly Redux helper framework",
|
||||||
|
"scripts": {
|
||||||
|
"docsify:serve": "docsify serve docs"
|
||||||
|
},
|
||||||
|
"version": "5.1.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/yanick/updux.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"redux",
|
||||||
|
"updeep"
|
||||||
|
],
|
||||||
|
"author": "Yanick Champoux <yanick@babyl.ca> (http://techblog.babyl.ca)",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/yanick/updux/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/yanick/updux#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@reduxjs/toolkit": "==2.0.0-alpha.5 ",
|
||||||
|
"@vitest/browser": "^0.23.1",
|
||||||
|
"@vitest/ui": "^0.23.1",
|
||||||
|
"docsify": "^4.13.1",
|
||||||
|
"eslint": "^8.22.0",
|
||||||
|
"eslint-plugin-no-only-tests": "^3.0.0",
|
||||||
|
"eslint-plugin-todo-plz": "^1.2.1",
|
||||||
|
"jsdoc-to-markdown": "^7.1.1",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"redux-toolkit": "^1.1.2",
|
||||||
|
"tsdoc-markdown": "^0.0.4",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"vite": "^4.2.1",
|
||||||
|
"vitest": "0.23.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
src/Updux.test.ts.2023-08-18
Normal file
13
src/Updux.test.ts.2023-08-18
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { test, expect } from 'vitest';
|
||||||
|
import Updux from './Updux.js';
|
||||||
|
|
||||||
|
test('subdux idempotency', () => {
|
||||||
|
const foo = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
a: new Updux({ initialState: 2 }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let fooState = foo.reducer(undefined, { type: 'noop' });
|
||||||
|
expect(foo.reducer(fooState, { type: 'noop' })).toBe(fooState);
|
||||||
|
});
|
448
src/Updux.ts
448
src/Updux.ts
@ -1,115 +1,198 @@
|
|||||||
|
import type { DuxConfig, DuxState } from './types.js';
|
||||||
|
import u from '@yanick/updeep-remeda';
|
||||||
|
import moize from 'moize/mjs/index.mjs';
|
||||||
|
import * as R from 'remeda';
|
||||||
|
import { expandAction, buildActions, DuxActions } from './actions.js';
|
||||||
import {
|
import {
|
||||||
|
Action,
|
||||||
|
ActionCreator,
|
||||||
|
AnyAction,
|
||||||
configureStore,
|
configureStore,
|
||||||
EnhancedStore,
|
EnhancedStore,
|
||||||
Action,
|
|
||||||
PayloadAction,
|
|
||||||
AnyAction,
|
|
||||||
ActionCreatorWithPreparedPayload,
|
|
||||||
} from '@reduxjs/toolkit';
|
} from '@reduxjs/toolkit';
|
||||||
import {
|
import { produce } from 'immer';
|
||||||
AugmentedMiddlewareAPI,
|
|
||||||
DuxActions,
|
|
||||||
DuxConfig,
|
|
||||||
DuxReaction,
|
|
||||||
DuxSelectors,
|
|
||||||
DuxState,
|
|
||||||
Mutation,
|
|
||||||
} from './types.js';
|
|
||||||
import baseMoize from 'moize/mjs/index.mjs';
|
|
||||||
import { buildInitialState } from './initialState.js';
|
|
||||||
import { buildActions } from './actions.js';
|
|
||||||
import { D } from '@mobily/ts-belt';
|
|
||||||
import { buildReducer } from './reducer.js';
|
import { buildReducer } from './reducer.js';
|
||||||
import { buildSelectors } from './selectors.js';
|
import { buildInitialState } from './initialState.js';
|
||||||
import { buildEffectsMiddleware, buildEffects, EffectMiddleware, augmentMiddlewareApi } from './effects.js';
|
import { buildSelectors, DuxSelectors } from './selectors.js';
|
||||||
import { augmentGetState, augmentDispatch } from './createStore.js';
|
import { AugmentedMiddlewareAPI, augmentGetState } from './createStore.js';
|
||||||
import { buildReactions } from './reactions.js';
|
import {
|
||||||
|
augmentMiddlewareApi,
|
||||||
|
buildEffects,
|
||||||
|
buildEffectsMiddleware,
|
||||||
|
EffectMiddleware,
|
||||||
|
} from './effects.js';
|
||||||
|
import buildSchema from './schema.js';
|
||||||
|
import Ajv from 'ajv';
|
||||||
|
|
||||||
type CreateStoreOptions<D> = Partial<{
|
export type Mutation<A = AnyAction, S = any> = (
|
||||||
preloadedState: DuxState<D>;
|
payload: A extends {
|
||||||
}>;
|
payload: infer P;
|
||||||
|
}
|
||||||
|
? P
|
||||||
|
: undefined,
|
||||||
|
action: A,
|
||||||
|
) => (state: S) => S | void;
|
||||||
|
|
||||||
interface ActionCreator<T extends string, P, A extends Array<any>> {
|
|
||||||
type: T;
|
function buildValidateMiddleware(schema) {
|
||||||
match: (
|
// @ts-ignore
|
||||||
action: Action<unknown>,
|
const ajv = new Ajv();
|
||||||
) => action is PayloadAction<P, T, never, never>;
|
const validate = ajv.compile(schema);
|
||||||
(...args: A): PayloadAction<P, T, never, never>;
|
return (api) => next => action => {
|
||||||
|
next(action);
|
||||||
|
const valid = validate(api.getState());
|
||||||
|
|
||||||
|
if (!valid) throw new Error("validation failed after action " + JSON.stringify(action) + "\n" + JSON.stringify(validate.errors));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const moize = (func) => baseMoize(func, { maxSize: 1 });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description main Updux class
|
|
||||||
*/
|
|
||||||
export default class Updux<D extends DuxConfig> {
|
export default class Updux<D extends DuxConfig> {
|
||||||
/** @internal */
|
#mutations = [];
|
||||||
#subduxes: {};
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#memoInitialState = moize(buildInitialState);
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#memoBuildReducer = moize(buildReducer);
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#memoBuildSelectors = moize(buildSelectors);
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#memoBuildEffects = moize(buildEffects);
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#memoBuildReactions = moize(buildReactions);
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#actions: Record<string, ActionCreator<string, any, any>> = {};
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#defaultMutation: {
|
|
||||||
terminal: boolean;
|
|
||||||
mutation: Mutation<any, DuxState<D>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#effects = [];
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#reactions: DuxReaction<D>[] = [];
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
#mutations: any[] = [];
|
|
||||||
|
|
||||||
constructor(private readonly duxConfig: D) {
|
constructor(private readonly duxConfig: D) {
|
||||||
if (duxConfig.subduxes)
|
// just to warn at creation time if config has issues
|
||||||
this.#subduxes = D.map(duxConfig.subduxes, (s) =>
|
this.actions;
|
||||||
s instanceof Updux ? s : new Updux(s),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#actions = buildActions(duxConfig.actions, this.#subduxes) as any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memoInitialState = moize(buildInitialState, {
|
||||||
|
maxSize: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
memoBuildActions = moize(buildActions, {
|
||||||
|
maxSize: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
memoBuildReducer = moize(buildReducer, { maxSize: 1 });
|
||||||
|
|
||||||
|
memoBuildSelectors = moize(buildSelectors, { maxSize: 1 });
|
||||||
|
|
||||||
|
memoBuildEffects = moize(buildEffects, { maxSize: 1 });
|
||||||
|
|
||||||
|
memoBuildSchema = moize(buildSchema, { maxSize: 1 });
|
||||||
|
|
||||||
|
get schema() {
|
||||||
|
return this.memoBuildSchema(
|
||||||
|
this.duxConfig.schema,
|
||||||
|
this.initialState,
|
||||||
|
this.duxConfig.subduxes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Initial state of the dux.
|
|
||||||
*/
|
|
||||||
get initialState(): DuxState<D> {
|
get initialState(): DuxState<D> {
|
||||||
return this.#memoInitialState(
|
return this.memoInitialState(
|
||||||
this.duxConfig.initialState,
|
this.duxConfig.initialState,
|
||||||
this.#subduxes,
|
this.duxConfig.subduxes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get subduxes() {
|
get actions(): DuxActions<D> {
|
||||||
return this.#subduxes;
|
return this.memoBuildActions(
|
||||||
|
this.duxConfig.actions,
|
||||||
|
this.duxConfig.subduxes,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toDux() {
|
||||||
|
return {
|
||||||
|
initialState: this.initialState,
|
||||||
|
actions: this.actions,
|
||||||
|
reducer: this.reducer,
|
||||||
|
effects: this.effects,
|
||||||
|
reactions: this.reactions,
|
||||||
|
selectors: this.selectors,
|
||||||
|
upreducer: this.upreducer,
|
||||||
|
schema: this.schema,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
get asDux() {
|
||||||
|
return this.toDux();
|
||||||
|
}
|
||||||
|
|
||||||
get actions(): DuxActions<D> {
|
get foo(): DuxActions<D> {
|
||||||
return this.#actions as any;
|
return true as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
get upreducer() {
|
||||||
|
const reducer = this.reducer;
|
||||||
|
return action => state => reducer(state, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO be smarter with the guard?
|
||||||
|
addMutation<A extends keyof DuxActions<D>>(
|
||||||
|
matcher: A,
|
||||||
|
mutation: Mutation<DuxActions<D>[A] extends (...args: any) => infer P ? P : never, DuxState<D>>,
|
||||||
|
terminal?: boolean,
|
||||||
|
): Updux<D>;
|
||||||
|
addMutation<A extends Action<any>>(
|
||||||
|
matcher: (action: A) => boolean,
|
||||||
|
mutation: Mutation<A, DuxState<D>>,
|
||||||
|
terminal?: boolean,
|
||||||
|
): Updux<D>;
|
||||||
|
addMutation<A extends ActionCreator<any>>(
|
||||||
|
actionCreator: A,
|
||||||
|
mutation: Mutation<ReturnType<A>, DuxState<D>>,
|
||||||
|
terminal?: boolean,
|
||||||
|
): Updux<D>;
|
||||||
|
addMutation(matcher, mutation, terminal = false) {
|
||||||
|
|
||||||
|
if (typeof matcher === 'string') {
|
||||||
|
if (!this.actions[matcher]) {
|
||||||
|
throw new Error(`action ${matcher} is unknown`);
|
||||||
|
}
|
||||||
|
matcher = this.actions[matcher];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof matcher === 'function' && matcher.match) {
|
||||||
|
// matcher, matcher man...
|
||||||
|
matcher = matcher.match;
|
||||||
|
}
|
||||||
|
|
||||||
|
//const immerMutation = (...args) => produce(mutation(...args));
|
||||||
|
|
||||||
|
this.#mutations = [
|
||||||
|
...this.#mutations,
|
||||||
|
{
|
||||||
|
terminal,
|
||||||
|
matcher,
|
||||||
|
mutation,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
#defaultMutation;
|
||||||
|
|
||||||
|
addDefaultMutation(
|
||||||
|
mutation: Mutation<any, DuxState<D>>,
|
||||||
|
terminal?: boolean,
|
||||||
|
);
|
||||||
|
addDefaultMutation(mutation, terminal = false) {
|
||||||
|
this.#defaultMutation = { terminal, mutation };
|
||||||
|
}
|
||||||
|
|
||||||
|
get reducer() {
|
||||||
|
return this.memoBuildReducer(
|
||||||
|
this.initialState,
|
||||||
|
this.#mutations,
|
||||||
|
this.#defaultMutation,
|
||||||
|
this.duxConfig.subduxes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectors(): DuxSelectors<D> {
|
||||||
|
return this.memoBuildSelectors(
|
||||||
|
this.duxConfig.selectors,
|
||||||
|
this.duxConfig.subduxes,
|
||||||
|
) as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
createStore(
|
createStore(
|
||||||
options: CreateStoreOptions<D> = {},
|
options: Partial<{
|
||||||
|
preloadedState: DuxState<D>;
|
||||||
|
validate: boolean;
|
||||||
|
buildMiddleware: (middleware: any[]) => any
|
||||||
|
}> = {},
|
||||||
): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> {
|
): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> {
|
||||||
const preloadedState = options.preloadedState;
|
const preloadedState = options.preloadedState;
|
||||||
|
|
||||||
@ -121,19 +204,32 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
|
|
||||||
let middleware = [effects];
|
let middleware = [effects];
|
||||||
|
|
||||||
const store: any = configureStore({
|
if (options.validate) {
|
||||||
preloadedState,
|
middleware.unshift(
|
||||||
|
buildValidateMiddleware(this.schema)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.buildMiddleware)
|
||||||
|
middleware = options.buildMiddleware(middleware);
|
||||||
|
|
||||||
|
const store = configureStore({
|
||||||
reducer: this.reducer,
|
reducer: this.reducer,
|
||||||
|
preloadedState,
|
||||||
middleware,
|
middleware,
|
||||||
});
|
});
|
||||||
|
|
||||||
store.dispatch = augmentDispatch(store.dispatch, this.actions);
|
const dispatch: any = store.dispatch;
|
||||||
|
for (const a in this.actions) {
|
||||||
|
dispatch[a] = (...args) => {
|
||||||
|
const action = (this.actions as any)[a](...args);
|
||||||
|
dispatch(action);
|
||||||
|
return action;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
store.getState = augmentGetState(store.getState, this.selectors);
|
store.getState = augmentGetState(store.getState, this.selectors);
|
||||||
|
|
||||||
store.actions = this.actions;
|
|
||||||
store.selectors = this.selectors;
|
|
||||||
|
|
||||||
for (const reaction of this.reactions) {
|
for (const reaction of this.reactions) {
|
||||||
let unsub;
|
let unsub;
|
||||||
const r = reaction(store);
|
const r = reaction(store);
|
||||||
@ -141,110 +237,32 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
unsub = store.subscribe(() => r(unsub));
|
unsub = store.subscribe(() => r(unsub));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(store as any).actions = this.actions;
|
||||||
|
(store as any).selectors = this.selectors;
|
||||||
|
|
||||||
return store as any;
|
return store as any;
|
||||||
|
|
||||||
|
// return store as ToolkitStore<AggregateState<T_LocalState, T_Subduxes>> &
|
||||||
|
// AugmentedMiddlewareAPI<
|
||||||
|
// AggregateState<T_LocalState, T_Subduxes>,
|
||||||
|
// AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
|
||||||
|
// AggregateSelectors<
|
||||||
|
// T_LocalSelectors,
|
||||||
|
// T_Subduxes,
|
||||||
|
// AggregateState<T_LocalState, T_Subduxes>
|
||||||
|
// >
|
||||||
|
// >;
|
||||||
}
|
}
|
||||||
|
|
||||||
addAction(action: ActionCreator<any, any, any>) {
|
#effects = [];
|
||||||
const { type } = action as any;
|
|
||||||
if (!this.#actions[type]) {
|
|
||||||
this.#actions = {
|
|
||||||
...this.#actions,
|
|
||||||
[type]: action,
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.#actions[type] !== action)
|
|
||||||
throw new Error(`redefining action ${type}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMutation<A extends keyof DuxActions<D>>(
|
addEffect(
|
||||||
actionType: A,
|
actionType: keyof DuxActions<D>,
|
||||||
mutation: Mutation<
|
effect: EffectMiddleware<D>,
|
||||||
DuxActions<D>[A] extends (...args: any[]) => infer R ? R : AnyAction,
|
|
||||||
DuxState<D>
|
|
||||||
>,
|
|
||||||
terminal?: boolean,
|
|
||||||
): Updux<D>;
|
): Updux<D>;
|
||||||
addMutation<AC extends ActionCreatorWithPreparedPayload<any, any, string, never, never>>(
|
addEffect(
|
||||||
actionCreator: AC,
|
actionCreator: { match: (action: any) => boolean },
|
||||||
mutation: Mutation<ReturnType<AC>, DuxState<D>>,
|
effect: EffectMiddleware<D>,
|
||||||
terminal?: boolean
|
|
||||||
): Updux<D & {
|
|
||||||
actions: Record<
|
|
||||||
AC extends { type: infer T } ? T : never
|
|
||||||
, AC
|
|
||||||
>
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
addMutation<A extends string, P, X extends Array<any>>(
|
|
||||||
actionCreator: ActionCreator<A, P, X>,
|
|
||||||
mutation: Mutation<ReturnType<ActionCreator<A, P, X>>, DuxState<D>>,
|
|
||||||
terminal?: boolean,
|
|
||||||
): Updux<D & {
|
|
||||||
actions: Record<A, ActionCreator<A, P, X>>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
addMutation(
|
|
||||||
matcher: (action: AnyAction) => boolean,
|
|
||||||
mutation: Mutation<AnyAction, DuxState<D>>,
|
|
||||||
terminal?: boolean,
|
|
||||||
): Updux<D>;
|
|
||||||
addMutation(actionCreator, mutation, terminal = false) {
|
|
||||||
|
|
||||||
if (typeof actionCreator === 'string') {
|
|
||||||
if (!this.actions[actionCreator]) throw new Error(`action ${actionCreator} not found`);
|
|
||||||
actionCreator = this.actions[actionCreator];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.addAction(actionCreator);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#mutations = this.#mutations.concat({
|
|
||||||
terminal,
|
|
||||||
matcher: (actionCreator as any).match ?? actionCreator,
|
|
||||||
mutation,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setDefaultMutation(
|
|
||||||
mutation: Mutation<any, DuxState<D>>,
|
|
||||||
terminal?: boolean,
|
|
||||||
);
|
|
||||||
setDefaultMutation(mutation, terminal = false) {
|
|
||||||
this.#defaultMutation = { terminal, mutation };
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
get reducer() {
|
|
||||||
return this.#memoBuildReducer(
|
|
||||||
this.initialState,
|
|
||||||
this.#mutations,
|
|
||||||
this.#defaultMutation,
|
|
||||||
this.#subduxes,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get selectors(): DuxSelectors<D> {
|
|
||||||
return this.#memoBuildSelectors(
|
|
||||||
this.duxConfig.selectors,
|
|
||||||
this.#subduxes,
|
|
||||||
) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
addEffect<A extends keyof DuxActions<D>>(
|
|
||||||
actionType: A,
|
|
||||||
effect: EffectMiddleware<D,
|
|
||||||
DuxActions<D>[A] extends (...args: any[]) => infer R ? R : AnyAction
|
|
||||||
>,
|
|
||||||
): Updux<D>;
|
|
||||||
addEffect<AC extends ActionCreatorWithPreparedPayload<any, any, string, never, never>>(
|
|
||||||
actionCreator: AC,
|
|
||||||
effect: EffectMiddleware<D, ReturnType<AC>>,
|
|
||||||
): Updux<D>;
|
): Updux<D>;
|
||||||
addEffect(
|
addEffect(
|
||||||
guardFunc: (action: AnyAction) => boolean,
|
guardFunc: (action: AnyAction) => boolean,
|
||||||
@ -280,45 +298,53 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#effects = this.#effects.concat(effect);
|
this.#effects = [...this.#effects, effect];
|
||||||
|
|
||||||
return this as any;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get effects(): any {
|
get effects(): any {
|
||||||
return this.#memoBuildEffects(this.#effects, this.#subduxes);
|
return this.memoBuildEffects(this.#effects, this.duxConfig.subduxes);
|
||||||
}
|
|
||||||
|
|
||||||
get upreducer() {
|
|
||||||
return (action: AnyAction) => (state?: DuxState<D>) => this.reducer(state, action);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#reactions = [];
|
||||||
addReaction(
|
addReaction(
|
||||||
reaction: DuxReaction<D>
|
reaction// :DuxReaction<D>
|
||||||
) {
|
) {
|
||||||
let previous: any;
|
let previous: any;
|
||||||
|
|
||||||
const memoized = (api: any) => {
|
const memoized = (api: any) => {
|
||||||
api = augmentMiddlewareApi(api, this.actions, this.selectors);
|
api = augmentMiddlewareApi(api, this.actions, this.selectors);
|
||||||
const r = reaction(api);
|
const r = reaction(api);
|
||||||
|
return (unsub: () => void) => {
|
||||||
const rMemoized = (localState, unsub) => {
|
const state = api.getState();
|
||||||
|
if (state === previous) return;
|
||||||
let p = previous;
|
let p = previous;
|
||||||
previous = localState;
|
previous = state;
|
||||||
r(localState, p, unsub);
|
r(state, p, unsub);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (unsub: () => void) => rMemoized(api.getState(), unsub);
|
|
||||||
};
|
};
|
||||||
|
this.#reactions = [
|
||||||
this.#reactions =
|
...this.#reactions, memoized
|
||||||
this.#reactions.concat(memoized as any);
|
]
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get reactions() {
|
get reactions() {
|
||||||
return this.#memoBuildReactions(this.#reactions, this.#subduxes);
|
return [
|
||||||
|
...this.#reactions,
|
||||||
|
...(Object.entries(this.duxConfig.subduxes ?? {}) as any).flatMap(
|
||||||
|
([slice, { reactions }]) =>
|
||||||
|
reactions.map(
|
||||||
|
(r) => (api, unsub) =>
|
||||||
|
r(
|
||||||
|
{
|
||||||
|
...api,
|
||||||
|
getState: () => api.getState()[slice],
|
||||||
|
},
|
||||||
|
unsub,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
import type { DuxConfig, DuxState } from './types.js';
|
|
||||||
import u from '@yanick/updeep-remeda';
|
|
||||||
import moize from 'moize/mjs/index.mjs';
|
|
||||||
import * as R from 'remeda';
|
|
||||||
import { expandAction, buildActions, DuxActions } from './actions.js';
|
|
||||||
import {
|
|
||||||
Action,
|
|
||||||
ActionCreator,
|
|
||||||
AnyAction,
|
|
||||||
configureStore,
|
|
||||||
EnhancedStore,
|
|
||||||
} from '@reduxjs/toolkit';
|
|
||||||
import { produce } from 'immer';
|
|
||||||
import { buildReducer } from './reducer.js';
|
|
||||||
import { buildInitialState } from './initialState.js';
|
|
||||||
import { buildSelectors, DuxSelectors } from './selectors.js';
|
|
||||||
import { AugmentedMiddlewareAPI, augmentGetState } from './createStore.js';
|
|
||||||
import {
|
|
||||||
augmentMiddlewareApi,
|
|
||||||
buildEffects,
|
|
||||||
buildEffectsMiddleware,
|
|
||||||
EffectMiddleware,
|
|
||||||
} from './effects.js';
|
|
||||||
import buildSchema from './schema.js';
|
|
||||||
import Ajv from 'ajv';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function buildValidateMiddleware(schema) {
|
|
||||||
// @ts-ignore
|
|
||||||
const ajv = new Ajv();
|
|
||||||
const validate = ajv.compile(schema);
|
|
||||||
return (api) => next => action => {
|
|
||||||
next(action);
|
|
||||||
const valid = validate(api.getState());
|
|
||||||
|
|
||||||
if (!valid) throw new Error("validation failed after action " + JSON.stringify(action) + "\n" + JSON.stringify(validate.errors));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Updux<D extends DuxConfig> {
|
|
||||||
#mutations = [];
|
|
||||||
|
|
||||||
get schema() {
|
|
||||||
return this.memoBuildSchema(
|
|
||||||
this.duxConfig.schema,
|
|
||||||
this.initialState,
|
|
||||||
this.duxConfig.subduxes,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
createStore(
|
|
||||||
options: Partial<{
|
|
||||||
preloadedState: DuxState<D>;
|
|
||||||
validate: boolean;
|
|
||||||
buildMiddleware: (middleware: any[]) => any
|
|
||||||
}> = {},
|
|
||||||
): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> {
|
|
||||||
const preloadedState = options.preloadedState;
|
|
||||||
|
|
||||||
|
|
||||||
if (options.validate) {
|
|
||||||
middleware.unshift(
|
|
||||||
buildValidateMiddleware(this.schema)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.buildMiddleware)
|
|
||||||
middleware = options.buildMiddleware(middleware);
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: this.reducer,
|
|
||||||
preloadedState,
|
|
||||||
middleware,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return store as any;
|
|
||||||
|
|
||||||
// return store as ToolkitStore<AggregateState<T_LocalState, T_Subduxes>> &
|
|
||||||
// AugmentedMiddlewareAPI<
|
|
||||||
// AggregateState<T_LocalState, T_Subduxes>,
|
|
||||||
// AggregateActions<ResolveActions<T_LocalActions>, T_Subduxes>,
|
|
||||||
// AggregateSelectors<
|
|
||||||
// T_LocalSelectors,
|
|
||||||
// T_Subduxes,
|
|
||||||
// AggregateState<T_LocalState, T_Subduxes>
|
|
||||||
// >
|
|
||||||
// >;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,6 +1,11 @@
|
|||||||
import { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
|
import Updux from './Updux.js';
|
||||||
import { buildActions, createAction, withPayload } from './actions.js';
|
import { createAction, withPayload, expandAction } from './actions.js';
|
||||||
import Updux from './index.js';
|
import type { ExpandedAction } from './actions.js';
|
||||||
|
import { expectTypeOf } from 'expect-type';
|
||||||
|
import {
|
||||||
|
ActionCreatorWithoutPayload,
|
||||||
|
ActionCreatorWithPreparedPayload,
|
||||||
|
} from '@reduxjs/toolkit';
|
||||||
|
|
||||||
test('basic action', () => {
|
test('basic action', () => {
|
||||||
const foo = createAction(
|
const foo = createAction(
|
||||||
@ -23,91 +28,187 @@ test('basic action', () => {
|
|||||||
}>();
|
}>();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('withPayload, just the type', () => {
|
test('Updux config accepts actions', () => {
|
||||||
const foo = createAction('foo', withPayload<string>());
|
const foo = new Updux({
|
||||||
|
|
||||||
expect(foo('bar')).toEqual({
|
|
||||||
type: 'foo',
|
|
||||||
payload: 'bar',
|
|
||||||
});
|
|
||||||
|
|
||||||
expectTypeOf(foo).parameters.toMatchTypeOf<[string]>();
|
|
||||||
|
|
||||||
expectTypeOf(foo).returns.toMatchTypeOf<{
|
|
||||||
type: 'foo';
|
|
||||||
payload: string;
|
|
||||||
}>();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('buildActions', () => {
|
|
||||||
const actions = buildActions(
|
|
||||||
{
|
|
||||||
one: createAction('one'), two: (x: string) => x,
|
|
||||||
withoutValue: null,
|
|
||||||
},
|
|
||||||
{ a: { actions: buildActions({ three: () => 3 }) } },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(actions.one()).toEqual({
|
|
||||||
type: 'one',
|
|
||||||
});
|
|
||||||
|
|
||||||
expectTypeOf(actions.one()).toMatchTypeOf<{
|
|
||||||
type: 'one';
|
|
||||||
}>();
|
|
||||||
|
|
||||||
expect(actions.two('potato')).toEqual({ type: 'two', payload: 'potato' });
|
|
||||||
|
|
||||||
expectTypeOf(actions.two('potato')).toMatchTypeOf<{
|
|
||||||
type: 'two';
|
|
||||||
payload: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
expect(actions.three()).toEqual({ type: 'three', payload: 3 });
|
|
||||||
|
|
||||||
expectTypeOf(actions.three()).toMatchTypeOf<{
|
|
||||||
type: 'three';
|
|
||||||
payload: number;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
expect(actions.withoutValue()).toEqual({ type: 'withoutValue' });
|
|
||||||
|
|
||||||
expectTypeOf(actions.withoutValue()).toMatchTypeOf<{
|
|
||||||
type: 'withoutValue';
|
|
||||||
}>();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Updux interactions', () => {
|
|
||||||
const dux = new Updux({
|
|
||||||
initialState: { a: 3 },
|
|
||||||
actions: {
|
actions: {
|
||||||
add: (x: number) => x,
|
one: createAction(
|
||||||
withNull: null,
|
'one',
|
||||||
|
withPayload((x) => ({ x })),
|
||||||
|
),
|
||||||
|
two: createAction(
|
||||||
|
'two',
|
||||||
|
withPayload((x) => x),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(Object.keys(foo.actions)).toHaveLength(2);
|
||||||
|
|
||||||
|
expect(foo.actions.one).toBeTypeOf('function');
|
||||||
|
expect(foo.actions.one('potato')).toEqual({
|
||||||
|
type: 'one',
|
||||||
|
payload: {
|
||||||
|
x: 'potato',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expectTypeOf(foo.actions.one).toMatchTypeOf<
|
||||||
|
ActionCreatorWithPreparedPayload<any, any, any, any>
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('expandAction', () => {
|
||||||
|
test('as-is', () => {
|
||||||
|
const result = expandAction(
|
||||||
|
createAction('foo', withPayload<boolean>()),
|
||||||
|
);
|
||||||
|
|
||||||
|
expectTypeOf(result).toMatchTypeOf<
|
||||||
|
(input: boolean) => { type: 'foo'; payload: boolean }
|
||||||
|
>();
|
||||||
|
|
||||||
|
expect(result(true)).toMatchObject({
|
||||||
|
type: 'foo',
|
||||||
|
payload: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('0', () => {
|
||||||
|
const result = expandAction(0, 'foo');
|
||||||
|
|
||||||
|
expectTypeOf(result).toMatchTypeOf<() => { type: 'foo' }>();
|
||||||
|
|
||||||
|
expect(result()).toMatchObject({
|
||||||
|
type: 'foo',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('function', () => {
|
||||||
|
const result = expandAction((size: number) => ({ size }), 'foo');
|
||||||
|
|
||||||
|
expectTypeOf(result).toMatchTypeOf<
|
||||||
|
() => { type: 'foo'; payload: { size: number } }
|
||||||
|
>();
|
||||||
|
|
||||||
|
expect(result(12)).toMatchObject({
|
||||||
|
type: 'foo',
|
||||||
|
payload: {
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('action types', () => {
|
||||||
|
const action1 = createAction('a1', withPayload<number>());
|
||||||
|
expectTypeOf(
|
||||||
|
true as any as ExpandedAction<typeof action1, 'a1'>,
|
||||||
|
).toMatchTypeOf<
|
||||||
|
ActionCreatorWithPreparedPayload<
|
||||||
|
[input: number],
|
||||||
|
number,
|
||||||
|
'a1',
|
||||||
|
never,
|
||||||
|
never
|
||||||
|
>
|
||||||
|
>();
|
||||||
|
const action2 = (input: boolean) => input;
|
||||||
|
let typed2: ExpandedAction<typeof action2, 'a2'>;
|
||||||
|
expectTypeOf(typed2).toMatchTypeOf<
|
||||||
|
ActionCreatorWithPreparedPayload<
|
||||||
|
[input: boolean],
|
||||||
|
boolean,
|
||||||
|
'a2',
|
||||||
|
never,
|
||||||
|
never
|
||||||
|
>
|
||||||
|
>();
|
||||||
|
let typed3: ExpandedAction<boolean, 'a3'>;
|
||||||
|
expectTypeOf(typed3).toMatchTypeOf<ActionCreatorWithoutPayload<'a3'>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('action definition shortcut', () => {
|
||||||
|
const foo = new Updux({
|
||||||
|
actions: {
|
||||||
|
foo: 0,
|
||||||
|
bar: (x: number) => ({ x }),
|
||||||
|
baz: createAction('baz', withPayload<boolean>()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(foo.actions.foo()).toEqual({ type: 'foo', payload: undefined });
|
||||||
|
expect(foo.actions.baz(false)).toEqual({
|
||||||
|
type: 'baz',
|
||||||
|
payload: false,
|
||||||
|
});
|
||||||
|
expect(foo.actions.bar(2)).toEqual({
|
||||||
|
type: 'bar',
|
||||||
|
payload: { x: 2 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expectTypeOf(foo.actions.foo).toMatchTypeOf<ActionCreatorWithoutPayload<'foo'>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('subduxes actions', () => {
|
||||||
|
const bar = createAction<number>('bar');
|
||||||
|
const baz = createAction('baz');
|
||||||
|
|
||||||
|
const foo = new Updux({
|
||||||
|
actions: {
|
||||||
|
bar,
|
||||||
},
|
},
|
||||||
subduxes: {
|
subduxes: {
|
||||||
subdux1: new Updux({
|
beta: {
|
||||||
actions: {
|
actions: {
|
||||||
fromSubdux: (x: string) => x
|
baz,
|
||||||
}
|
},
|
||||||
}),
|
},
|
||||||
subdux2: {
|
// to check if we can deal with empty actions
|
||||||
actions: {
|
gamma: {},
|
||||||
ohmy: () => { }
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('actions getter', () => {
|
expect(foo.actions).toHaveProperty('bar');
|
||||||
expect(dux.actions.add).toBeTruthy();
|
expect(foo.actions).toHaveProperty('baz');
|
||||||
});
|
|
||||||
|
|
||||||
expectTypeOf(dux.actions.withNull).toMatchTypeOf<
|
expect(foo.actions.bar(2)).toHaveProperty('type', 'bar');
|
||||||
ActionCreatorWithPreparedPayload<any, null, 'withNull'>>();
|
expect(foo.actions.baz()).toHaveProperty('type', 'baz');
|
||||||
|
|
||||||
expectTypeOf(dux.actions).toMatchTypeOf<Record<string, any>>();
|
|
||||||
expectTypeOf(dux.actions?.add).not.toEqualTypeOf<any>();
|
|
||||||
|
|
||||||
expect(dux.actions.fromSubdux('payload')).toEqual({ type: 'fromSubdux', payload: 'payload' });
|
|
||||||
|
|
||||||
|
expectTypeOf(foo.actions.baz).toMatchTypeOf<
|
||||||
|
ActionCreatorWithoutPayload<'baz'>
|
||||||
|
>();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throw if double action', () => {
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
new Updux({
|
||||||
|
actions: {
|
||||||
|
foo: createAction('foo'),
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
beta: {
|
||||||
|
actions: {
|
||||||
|
foo: createAction('foo'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow(/action 'foo' defined both locally and in subdux 'beta'/);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
new Updux({
|
||||||
|
subduxes: {
|
||||||
|
gamma: {
|
||||||
|
actions: {
|
||||||
|
foo: createAction('foo'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beta: {
|
||||||
|
actions: {
|
||||||
|
foo: createAction('foo'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toThrow(/action 'foo' defined both in subduxes 'gamma' and 'beta'/);
|
||||||
});
|
});
|
||||||
|
104
src/actions.ts
104
src/actions.ts
@ -1,8 +1,54 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
import {
|
||||||
import { D } from '@mobily/ts-belt';
|
ActionCreator,
|
||||||
import { DuxActions, DuxConfig, Subduxes } from './types.js';
|
ActionCreatorWithoutPayload,
|
||||||
|
createAction,
|
||||||
|
} from '@reduxjs/toolkit';
|
||||||
export { createAction } from '@reduxjs/toolkit';
|
export { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
FromSchema,
|
||||||
|
SubduxesState,
|
||||||
|
DuxSchema,
|
||||||
|
DuxConfig,
|
||||||
|
UnionToIntersection,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
import * as R from 'remeda';
|
||||||
|
|
||||||
|
export type DuxActions<D> = (D extends { actions: infer A }
|
||||||
|
? {
|
||||||
|
[key in keyof A]: key extends string
|
||||||
|
? ExpandedAction<A[key], key>
|
||||||
|
: never;
|
||||||
|
}
|
||||||
|
: {}) &
|
||||||
|
UnionToIntersection<
|
||||||
|
D extends { subduxes: infer S } ? DuxActions<S[keyof S]> : {}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ExpandedAction<P, T extends string> = P extends (...a: any[]) => {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
? P
|
||||||
|
: P extends (...a: any[]) => any
|
||||||
|
? ActionCreatorWithPreparedPayload<
|
||||||
|
Parameters<P>,
|
||||||
|
ReturnType<P>,
|
||||||
|
T,
|
||||||
|
never,
|
||||||
|
never
|
||||||
|
>
|
||||||
|
: ActionCreatorWithoutPayload<T>;
|
||||||
|
|
||||||
|
export type DuxState<D> = (D extends { schema: Record<string, any> }
|
||||||
|
? FromSchema<DuxSchema<D>>
|
||||||
|
: D extends { initialState: infer INITIAL_STATE }
|
||||||
|
? INITIAL_STATE
|
||||||
|
: {}) &
|
||||||
|
(D extends { subduxes: Record<string, DuxConfig> }
|
||||||
|
? SubduxesState<D>
|
||||||
|
: unknown);
|
||||||
|
|
||||||
export function withPayload<P>(): (input: P) => { payload: P };
|
export function withPayload<P>(): (input: P) => { payload: P };
|
||||||
export function withPayload<P, A extends any[]>(
|
export function withPayload<P, A extends any[]>(
|
||||||
@ -14,17 +60,20 @@ export function withPayload(prepare = (input) => input) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildActions<L extends DuxConfig['actions']>(
|
export function expandAction(prepare, actionType?: string) {
|
||||||
localActions: L,
|
if (typeof prepare === 'function' && prepare.type) return prepare;
|
||||||
): DuxActions<{ actions: L }>;
|
|
||||||
export function buildActions<L extends DuxActions<any>, S extends Subduxes>(
|
if (typeof prepare === 'function') {
|
||||||
localActions: L,
|
return createAction(actionType, withPayload(prepare));
|
||||||
subduxes: S,
|
}
|
||||||
): DuxActions<{ actions: L; subduxes: S }>;
|
|
||||||
|
if (actionType) {
|
||||||
|
return createAction(actionType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function buildActions(localActions = {}, subduxes = {}) {
|
export function buildActions(localActions = {}, subduxes = {}) {
|
||||||
localActions = D.mapWithKey(localActions, (key, value) =>
|
localActions = R.mapValues(localActions, expandAction);
|
||||||
expandAction(value, String(key)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let actions: Record<string, string> = {};
|
let actions: Record<string, string> = {};
|
||||||
|
|
||||||
@ -41,27 +90,18 @@ export function buildActions(localActions = {}, subduxes = {}) {
|
|||||||
}
|
}
|
||||||
actions[a] = slice;
|
actions[a] = slice;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const a in localActions) {
|
for (const a in localActions) {
|
||||||
if (actions[a]) {
|
if (actions[a]) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`action '${a}' defined both locally and in subdux '${actions[a]}'`,
|
`action '${a}' defined both locally and in subdux '${actions[a]}'`,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return R.mergeAll([
|
||||||
localActions,
|
localActions,
|
||||||
...D.values(subduxes).map((s: any) => s.actions ?? {}),
|
...Object.values(subduxes).map(R.pathOr<any, any>(['actions'], {})),
|
||||||
].reduce(D.merge);
|
]) as any;
|
||||||
}
|
|
||||||
|
|
||||||
export function expandAction(prepare, actionType?: string) {
|
|
||||||
if (typeof prepare === 'function' && prepare.type) return prepare;
|
|
||||||
|
|
||||||
if (typeof prepare === 'function')
|
|
||||||
return createAction(actionType, withPayload(prepare));
|
|
||||||
|
|
||||||
if (actionType) return createAction(actionType);
|
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,67 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { expectTypeOf } from 'expect-type';
|
||||||
import { test, expect } from 'vitest';
|
import { test, expect } from 'vitest';
|
||||||
import Updux, { createAction, withPayload } from './index.js';
|
import Updux from './Updux.js';
|
||||||
|
|
||||||
const incr = createAction('incr', withPayload<number>());
|
|
||||||
|
|
||||||
const dux = new Updux({
|
const dux = new Updux({
|
||||||
initialState: 1,
|
initialState: 'a',
|
||||||
// selectors: {
|
actions: {
|
||||||
// double: (x: string) => x + x,
|
action1: 0,
|
||||||
// },
|
},
|
||||||
}).addMutation(incr, (i, action) => state => {
|
selectors: {
|
||||||
expectTypeOf(i).toEqualTypeOf<number>();
|
double: (x: string) => x + x,
|
||||||
expectTypeOf(state).toEqualTypeOf<number>();
|
},
|
||||||
expectTypeOf(action).toEqualTypeOf<{ type: 'incr', payload: number; }>();
|
|
||||||
return state + i
|
|
||||||
});
|
});
|
||||||
|
|
||||||
suite.only('store dispatch actions', () => {
|
dux.addMutation(dux.actions.action1, () => (state) => 'mutation1');
|
||||||
|
|
||||||
|
test('createStore', () => {
|
||||||
|
expect(dux.createStore().getState()).toEqual('a');
|
||||||
|
expect(dux.createStore({ preloadedState: 'b' }).getState()).toEqual('b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('augmentGetState', () => {
|
||||||
const store = dux.createStore();
|
const store = dux.createStore();
|
||||||
|
|
||||||
test('dispatch actions', () => {
|
expect(store.getState()).toEqual('a');
|
||||||
expect(store.dispatch.incr).toBeTypeOf('function');
|
|
||||||
expectTypeOf(store.dispatch.incr).toMatchTypeOf<Function>();
|
expectTypeOf(store.getState()).toMatchTypeOf<string>();
|
||||||
|
expectTypeOf(store.getState.double()).toMatchTypeOf<string>();
|
||||||
|
|
||||||
|
expect(store.getState.double()).toEqual('aa');
|
||||||
|
|
||||||
|
expect(store.actions.action1).toBeTypeOf('function');
|
||||||
|
|
||||||
|
expect(store.dispatch.action1()).toMatchObject({ type: 'action1' });
|
||||||
|
|
||||||
|
expect(store.getState.double()).toEqual('mutation1mutation1');
|
||||||
|
|
||||||
|
// selectors
|
||||||
|
expect(store.selectors.double).toBeTypeOf('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mutations of subduxes', () => {
|
||||||
|
const incr = createAction('incr');
|
||||||
|
|
||||||
|
const subdux1 = new Updux({
|
||||||
|
actions: { incr },
|
||||||
|
initialState: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reducer does something', () => {
|
subdux1.addMutation(incr, () => (state) => state + 1);
|
||||||
store.dispatch.incr(7);
|
|
||||||
expect(store.getState()).toEqual(8);
|
const dux = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
subdux1: subdux1.asDux,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const store = dux.createStore();
|
||||||
|
|
||||||
|
expect(store.getState()).toMatchObject({ subdux1: 0 });
|
||||||
|
|
||||||
|
store.dispatch.incr();
|
||||||
|
expect(store.getState()).toMatchObject({ subdux1: 1 });
|
||||||
|
store.dispatch.incr();
|
||||||
|
expect(store.getState()).toMatchObject({ subdux1: 2 });
|
||||||
});
|
});
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user