Compare commits
10 Commits
7280381ca8
...
66c2b162db
Author | SHA1 | Date | |
---|---|---|---|
66c2b162db | |||
edb6716c9c | |||
88e1565fa5 | |||
b821f3669f | |||
38949d9e0f | |||
b4c1b357e4 | |||
2e4b0900ea | |||
af5f2d0a8f | |||
e3e9c9a24b | |||
c438847051 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ 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,2 +1,9 @@
|
|||||||
- documentation generator (mkdocs + jsdoc-to-markdown)
|
* documentation action + mutation <<<
|
||||||
- createStore
|
* createAction
|
||||||
|
* addMutation
|
||||||
|
|
||||||
|
addMutation with signature
|
||||||
|
|
||||||
|
.addMutation( actionCreator, (payload,action) => state => newState )
|
||||||
|
|
||||||
|
* then check that the mutation is in the new type
|
||||||
|
@ -20,7 +20,8 @@ tasks:
|
|||||||
- git checkout {{.PARENT_BRANCH}}
|
- git checkout {{.PARENT_BRANCH}}
|
||||||
- git weld -
|
- git weld -
|
||||||
|
|
||||||
test: vitest run src docs/*.ts
|
test: vitest run src
|
||||||
|
|
||||||
test:dev: vitest src
|
test:dev: vitest src
|
||||||
|
|
||||||
lint:fix:delta:
|
lint:fix:delta:
|
||||||
@ -65,6 +66,20 @@ 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
|
||||||
|
17
contrib/api_grooming.pl
Executable file
17
contrib/api_grooming.pl
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/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});
|
||||||
|
|
226
docs-docsidy/README.md
Normal file
226
docs-docsidy/README.md
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
# 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
docs-docsidy/api/.nojekyll
Normal file
1
docs-docsidy/api/.nojekyll
Normal file
@ -0,0 +1 @@
|
|||||||
|
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
|
228
docs-docsidy/api/README.md
Normal file
228
docs-docsidy/api/README.md
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
435
docs-docsidy/api/classes/default.md
Normal file
435
docs-docsidy/api/classes/default.md
Normal file
@ -0,0 +1,435 @@
|
|||||||
|
[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` |
|
129
docs-docsidy/api/modules.md
Normal file
129
docs-docsidy/api/modules.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
[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` |
|
@ -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/buble.css">
|
<link rel="stylesheet" href="docsify/themes/vue.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
115
docs-docsidy/tutorial.md
Normal file
115
docs-docsidy/tutorial.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
--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
Normal file
21
docs/.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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
Normal file
4
docs/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
docs/.vscode/launch.json
vendored
Normal file
11
docs/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"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,226 +1,55 @@
|
|||||||
# What's Updux?
|
# Starlight Starter Kit: Basics
|
||||||
|
|
||||||
So, I'm a fan of [Redux](https://redux.js.org). Two days ago I discovered
|
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
|
||||||
[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';
|
npm create astro@latest -- --template starlight
|
||||||
|
|
||||||
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
|
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
|
||||||
|
[![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)
|
||||||
|
|
||||||
Full documentation can be [found here](https://yanick.github.io/updux/).
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
Right now the best way to understand the whole thing is to go
|
|
||||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
|
||||||
|
|
||||||
## Exporting upduxes
|
## 🚀 Project Structure
|
||||||
|
|
||||||
If you are creating upduxes that will be used as subduxes
|
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||||
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/
|
||||||
const updux = new Updux({ ... });
|
├── src/
|
||||||
|
│ ├── assets/
|
||||||
export default updux;
|
│ ├── content/
|
||||||
|
│ │ ├── docs/
|
||||||
|
│ │ └── config.ts
|
||||||
|
│ └── env.d.ts
|
||||||
|
├── astro.config.mjs
|
||||||
|
├── package.json
|
||||||
|
└── tsconfig.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can use them as subduxes like this:
|
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.
|
||||||
|
|
||||||
```
|
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
|
|
||||||
|
|
||||||
const updux = new Updux({
|
Static assets, like favicons, can be placed in the `public/` directory.
|
||||||
subduxes: {
|
|
||||||
foo, bar
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if you want to use it:
|
## 🧞 Commands
|
||||||
|
|
||||||
```
|
All commands are run from the root of the project, from a terminal:
|
||||||
import updux from './myUpdux';
|
|
||||||
|
|
||||||
const {
|
| Command | Action |
|
||||||
reducer,
|
| :------------------------ | :----------------------------------------------- |
|
||||||
actions: { doTheThing },
|
| `npm install` | Installs dependencies |
|
||||||
createStore,
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
middleware,
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
} = updux;
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
```
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
## Mapping a mutation to all values of a state
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
Say you have a `todos` state that is an array of `todo` sub-states. It's easy
|
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).
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
```
|
|
||||||
|
54
docs/astro.config.mjs
Normal file
54
docs/astro.config.mjs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
@ -1,19 +0,0 @@
|
|||||||
/// [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]
|
|
19
docs/package.json
Normal file
19
docs/package.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
177
docs/public/favicon.svg
Normal file
177
docs/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 583 KiB |
177
docs/public/updux-logo.svg
Normal file
177
docs/public/updux-logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 583 KiB |
@ -1,53 +0,0 @@
|
|||||||
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
|
|
||||||
]);
|
|
||||||
|
|
||||||
});
|
|
BIN
docs/src/assets/houston.webp
Normal file
BIN
docs/src/assets/houston.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
6
docs/src/content/config.ts
Normal file
6
docs/src/content/config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { defineCollection } from 'astro:content';
|
||||||
|
import { docsSchema } from '@astrojs/starlight/schema';
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
docs: defineCollection({ schema: docsSchema() }),
|
||||||
|
};
|
1
docs/src/content/docs/api/.nojekyll
Normal file
1
docs/src/content/docs/api/.nojekyll
Normal file
@ -0,0 +1 @@
|
|||||||
|
TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.
|
153
docs/src/content/docs/api/README.md
Normal file
153
docs/src/content/docs/api/README.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
355
docs/src/content/docs/api/classes/default.md
Normal file
355
docs/src/content/docs/api/classes/default.md
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
---
|
||||||
|
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`
|
129
docs/src/content/docs/api/modules.md
Normal file
129
docs/src/content/docs/api/modules.md
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
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` |
|
47
docs/src/content/docs/guides/001-introduction.mdx
Normal file
47
docs/src/content/docs/guides/001-introduction.mdx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
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/
|
32
docs/src/content/docs/guides/002-state-definition.mdx
Normal file
32
docs/src/content/docs/guides/002-state-definition.mdx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
});
|
||||||
|
```
|
43
docs/src/content/docs/guides/003-selectors.md
Normal file
43
docs/src/content/docs/guides/003-selectors.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
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);
|
||||||
|
```
|
96
docs/src/content/docs/guides/004-actions.mdx
Normal file
96
docs/src/content/docs/guides/004-actions.mdx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
---
|
||||||
|
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') );
|
||||||
|
|
||||||
|
|
||||||
|
```
|
86
docs/src/content/docs/guides/005-mutations.md
Normal file
86
docs/src/content/docs/guides/005-mutations.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
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 );
|
||||||
|
```
|
47
docs/src/content/docs/guides/010-effects.md
Normal file
47
docs/src/content/docs/guides/010-effects.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
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 );
|
||||||
|
``````
|
28
docs/src/content/docs/guides/020-subduxes.md
Normal file
28
docs/src/content/docs/guides/020-subduxes.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
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.
|
242
docs/src/content/docs/index.mdx
Normal file
242
docs/src/content/docs/index.mdx
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
11
docs/src/content/docs/reference/example.md
Normal file
11
docs/src/content/docs/reference/example.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
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
|
74
docs/src/content/docs/tutorial/actions.mdx
Normal file
74
docs/src/content/docs/tutorial/actions.mdx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
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
docs/src/content/docs/tutorial/code
Symbolic link
1
docs/src/content/docs/tutorial/code
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../../../../src/tutorial
|
14
docs/src/content/docs/tutorial/extractSnippet.js
Normal file
14
docs/src/content/docs/tutorial/extractSnippet.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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");
|
||||||
|
}
|
20
docs/src/content/docs/tutorial/initialState.mdx
Normal file
20
docs/src/content/docs/tutorial/initialState.mdx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
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" />
|
23
docs/src/content/docs/tutorial/intro.md
Normal file
23
docs/src/content/docs/tutorial/intro.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
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/
|
24
docs/src/content/docs/tutorial/mutations.mdx
Normal file
24
docs/src/content/docs/tutorial/mutations.mdx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
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
Normal file
2
docs/src/env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
@ -1,28 +0,0 @@
|
|||||||
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;
|
|
@ -1,26 +0,0 @@
|
|||||||
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;
|
|
10
docs/tsconfig.json
Normal file
10
docs/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"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/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +0,0 @@
|
|||||||
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: []
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,25 +0,0 @@
|
|||||||
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: []
|
|
||||||
})
|
|
||||||
});
|
|
@ -1,56 +0,0 @@
|
|||||||
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,37 +0,0 @@
|
|||||||
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,42 +0,0 @@
|
|||||||
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,39 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
@ -1,27 +0,0 @@
|
|||||||
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,91 +0,0 @@
|
|||||||
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
168
docs/tutorial.md
@ -1,168 +0,0 @@
|
|||||||
# 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/
|
|
@ -1,26 +0,0 @@
|
|||||||
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'
|
|
||||||
})
|
|
||||||
});
|
|
8
guides/+page.js
Normal file
8
guides/+page.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const prerender = true;
|
||||||
|
|
||||||
|
/** @type {import('./$types').PageLoad} */
|
||||||
|
export function load() {
|
||||||
|
throw redirect(307, '/docs/first-category/first-page');
|
||||||
|
}
|
47
guides/[...001]introduction.md
Normal file
47
guides/[...001]introduction.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
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
Normal file
16
mkdocs.yml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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,59 +1,62 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@yanick/updeep-remeda": "^2.2.0",
|
"@mobily/ts-belt": "^3.13.1",
|
||||||
"ajv": "^8.12.0",
|
"@yanick/updeep-remeda": "^2.3.1",
|
||||||
"expect-type": "^0.16.0",
|
"ajv": "^8.12.0",
|
||||||
"immer": "^9.0.15",
|
"expect-type": "^0.16.0",
|
||||||
"json-schema-shorthand": "^2.0.0",
|
"immer": "^9.0.15",
|
||||||
"json-schema-to-ts": "^2.9.2",
|
"json-schema-shorthand": "^2.0.0",
|
||||||
"memoize-one": "^6.0.0",
|
"json-schema-to-ts": "^2.9.2",
|
||||||
"moize": "^6.1.6",
|
"memoize-one": "^6.0.0",
|
||||||
"redux": "^4.2.0",
|
"moize": "^6.1.6",
|
||||||
"remeda": "^1.0.1",
|
"redux": "^4.2.0",
|
||||||
"updeep": "^1.2.1"
|
"remeda": "^1.0.1",
|
||||||
},
|
"updeep": "^1.2.1"
|
||||||
"license": "MIT",
|
},
|
||||||
"module": "dist/index.js",
|
"license": "MIT",
|
||||||
"types": "./dist/index.d.ts",
|
"module": "dist/index.js",
|
||||||
"exports": {
|
"types": "./dist/index.d.ts",
|
||||||
".": {
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
452
src/Updux.ts
452
src/Updux.ts
@ -1,198 +1,115 @@
|
|||||||
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 { 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 {
|
import {
|
||||||
augmentMiddlewareApi,
|
AugmentedMiddlewareAPI,
|
||||||
buildEffects,
|
DuxActions,
|
||||||
buildEffectsMiddleware,
|
DuxConfig,
|
||||||
EffectMiddleware,
|
DuxReaction,
|
||||||
} from './effects.js';
|
DuxSelectors,
|
||||||
import buildSchema from './schema.js';
|
DuxState,
|
||||||
import Ajv from 'ajv';
|
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 { buildSelectors } from './selectors.js';
|
||||||
|
import { buildEffectsMiddleware, buildEffects, EffectMiddleware, augmentMiddlewareApi } from './effects.js';
|
||||||
|
import { augmentGetState, augmentDispatch } from './createStore.js';
|
||||||
|
import { buildReactions } from './reactions.js';
|
||||||
|
|
||||||
export type Mutation<A = AnyAction, S = any> = (
|
type CreateStoreOptions<D> = Partial<{
|
||||||
payload: A extends {
|
preloadedState: DuxState<D>;
|
||||||
payload: infer P;
|
}>;
|
||||||
}
|
|
||||||
? P
|
|
||||||
: undefined,
|
|
||||||
action: A,
|
|
||||||
) => (state: S) => S | void;
|
|
||||||
|
|
||||||
|
interface ActionCreator<T extends string, P, A extends Array<any>> {
|
||||||
function buildValidateMiddleware(schema) {
|
type: T;
|
||||||
// @ts-ignore
|
match: (
|
||||||
const ajv = new Ajv();
|
action: Action<unknown>,
|
||||||
const validate = ajv.compile(schema);
|
) => action is PayloadAction<P, T, never, never>;
|
||||||
return (api) => next => action => {
|
(...args: A): PayloadAction<P, T, never, never>;
|
||||||
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> {
|
||||||
#mutations = [];
|
/** @internal */
|
||||||
|
#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) {
|
||||||
// just to warn at creation time if config has issues
|
if (duxConfig.subduxes)
|
||||||
this.actions;
|
this.#subduxes = D.map(duxConfig.subduxes, (s) =>
|
||||||
|
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.duxConfig.subduxes,
|
this.#subduxes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get subduxes() {
|
||||||
|
return this.#subduxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
get actions(): DuxActions<D> {
|
get actions(): DuxActions<D> {
|
||||||
return this.memoBuildActions(
|
return this.#actions as any;
|
||||||
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 foo(): DuxActions<D> {
|
|
||||||
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: Partial<{
|
options: CreateStoreOptions<D> = {},
|
||||||
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;
|
||||||
|
|
||||||
@ -204,32 +121,19 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
|
|
||||||
let middleware = [effects];
|
let middleware = [effects];
|
||||||
|
|
||||||
if (options.validate) {
|
const store: any = configureStore({
|
||||||
middleware.unshift(
|
|
||||||
buildValidateMiddleware(this.schema)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.buildMiddleware)
|
|
||||||
middleware = options.buildMiddleware(middleware);
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: this.reducer,
|
|
||||||
preloadedState,
|
preloadedState,
|
||||||
|
reducer: this.reducer,
|
||||||
middleware,
|
middleware,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dispatch: any = store.dispatch;
|
store.dispatch = augmentDispatch(store.dispatch, this.actions);
|
||||||
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);
|
||||||
@ -237,32 +141,110 @@ 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>
|
|
||||||
// >
|
|
||||||
// >;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#effects = [];
|
addAction(action: ActionCreator<any, any, any>) {
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
addEffect(
|
addMutation<A extends keyof DuxActions<D>>(
|
||||||
actionType: keyof DuxActions<D>,
|
actionType: A,
|
||||||
effect: EffectMiddleware<D>,
|
mutation: Mutation<
|
||||||
|
DuxActions<D>[A] extends (...args: any[]) => infer R ? R : AnyAction,
|
||||||
|
DuxState<D>
|
||||||
|
>,
|
||||||
|
terminal?: boolean,
|
||||||
): Updux<D>;
|
): Updux<D>;
|
||||||
addEffect(
|
addMutation<AC extends ActionCreatorWithPreparedPayload<any, any, string, never, never>>(
|
||||||
actionCreator: { match: (action: any) => boolean },
|
actionCreator: AC,
|
||||||
effect: EffectMiddleware<D>,
|
mutation: Mutation<ReturnType<AC>, DuxState<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,
|
||||||
@ -298,53 +280,45 @@ export default class Updux<D extends DuxConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#effects = [...this.#effects, effect];
|
this.#effects = this.#effects.concat(effect);
|
||||||
|
|
||||||
return this;
|
return this as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
get effects(): any {
|
get effects(): any {
|
||||||
return this.memoBuildEffects(this.#effects, this.duxConfig.subduxes);
|
return this.#memoBuildEffects(this.#effects, this.#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 state = api.getState();
|
const rMemoized = (localState, unsub) => {
|
||||||
if (state === previous) return;
|
|
||||||
let p = previous;
|
let p = previous;
|
||||||
previous = state;
|
previous = localState;
|
||||||
r(state, p, unsub);
|
r(localState, p, unsub);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return (unsub: () => void) => rMemoized(api.getState(), unsub);
|
||||||
};
|
};
|
||||||
this.#reactions = [
|
|
||||||
...this.#reactions, memoized
|
this.#reactions =
|
||||||
]
|
this.#reactions.concat(memoized as any);
|
||||||
|
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
get reactions() {
|
get reactions() {
|
||||||
return [
|
return this.#memoBuildReactions(this.#reactions, this.#subduxes);
|
||||||
...this.#reactions,
|
|
||||||
...(Object.entries(this.duxConfig.subduxes ?? {}) as any).flatMap(
|
|
||||||
([slice, { reactions }]) =>
|
|
||||||
reactions.map(
|
|
||||||
(r) => (api, unsub) =>
|
|
||||||
r(
|
|
||||||
{
|
|
||||||
...api,
|
|
||||||
getState: () => api.getState()[slice],
|
|
||||||
},
|
|
||||||
unsub,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
94
src/Updux.ts.2024-07-29
Normal file
94
src/Updux.ts.2024-07-29
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
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,11 +1,6 @@
|
|||||||
import Updux from './Updux.js';
|
import { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
|
||||||
import { createAction, withPayload, expandAction } from './actions.js';
|
import { buildActions, createAction, withPayload } from './actions.js';
|
||||||
import type { ExpandedAction } from './actions.js';
|
import Updux from './index.js';
|
||||||
import { expectTypeOf } from 'expect-type';
|
|
||||||
import {
|
|
||||||
ActionCreatorWithoutPayload,
|
|
||||||
ActionCreatorWithPreparedPayload,
|
|
||||||
} from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
test('basic action', () => {
|
test('basic action', () => {
|
||||||
const foo = createAction(
|
const foo = createAction(
|
||||||
@ -28,187 +23,91 @@ test('basic action', () => {
|
|||||||
}>();
|
}>();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Updux config accepts actions', () => {
|
test('withPayload, just the type', () => {
|
||||||
const foo = new Updux({
|
const foo = createAction('foo', withPayload<string>());
|
||||||
actions: {
|
|
||||||
one: createAction(
|
expect(foo('bar')).toEqual({
|
||||||
'one',
|
type: 'foo',
|
||||||
withPayload((x) => ({ x })),
|
payload: 'bar',
|
||||||
),
|
|
||||||
two: createAction(
|
|
||||||
'two',
|
|
||||||
withPayload((x) => x),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(foo.actions)).toHaveLength(2);
|
expectTypeOf(foo).parameters.toMatchTypeOf<[string]>();
|
||||||
|
|
||||||
expect(foo.actions.one).toBeTypeOf('function');
|
expectTypeOf(foo).returns.toMatchTypeOf<{
|
||||||
expect(foo.actions.one('potato')).toEqual({
|
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',
|
type: 'one',
|
||||||
payload: {
|
|
||||||
x: 'potato',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expectTypeOf(foo.actions.one).toMatchTypeOf<
|
expectTypeOf(actions.one()).toMatchTypeOf<{
|
||||||
ActionCreatorWithPreparedPayload<any, any, any, any>
|
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('expandAction', () => {
|
describe('Updux interactions', () => {
|
||||||
test('as-is', () => {
|
const dux = new Updux({
|
||||||
const result = expandAction(
|
initialState: { a: 3 },
|
||||||
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: {
|
actions: {
|
||||||
foo: 0,
|
add: (x: number) => x,
|
||||||
bar: (x: number) => ({ x }),
|
withNull: null,
|
||||||
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: {
|
||||||
beta: {
|
subdux1: new Updux({
|
||||||
actions: {
|
actions: {
|
||||||
baz,
|
fromSubdux: (x: string) => x
|
||||||
},
|
}
|
||||||
},
|
}),
|
||||||
// to check if we can deal with empty actions
|
subdux2: {
|
||||||
gamma: {},
|
actions: {
|
||||||
},
|
ohmy: () => { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(foo.actions).toHaveProperty('bar');
|
test('actions getter', () => {
|
||||||
expect(foo.actions).toHaveProperty('baz');
|
expect(dux.actions.add).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
expect(foo.actions.bar(2)).toHaveProperty('type', 'bar');
|
expectTypeOf(dux.actions.withNull).toMatchTypeOf<
|
||||||
expect(foo.actions.baz()).toHaveProperty('type', 'baz');
|
ActionCreatorWithPreparedPayload<any, null, 'withNull'>>();
|
||||||
|
|
||||||
|
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,54 +1,8 @@
|
|||||||
import {
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
ActionCreator,
|
import { D } from '@mobily/ts-belt';
|
||||||
ActionCreatorWithoutPayload,
|
import { DuxActions, DuxConfig, Subduxes } from './types.js';
|
||||||
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[]>(
|
||||||
@ -60,20 +14,17 @@ export function withPayload(prepare = (input) => input) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expandAction(prepare, actionType?: string) {
|
export function buildActions<L extends DuxConfig['actions']>(
|
||||||
if (typeof prepare === 'function' && prepare.type) return prepare;
|
localActions: L,
|
||||||
|
): DuxActions<{ actions: L }>;
|
||||||
if (typeof prepare === 'function') {
|
export function buildActions<L extends DuxActions<any>, S extends Subduxes>(
|
||||||
return createAction(actionType, withPayload(prepare));
|
localActions: L,
|
||||||
}
|
subduxes: S,
|
||||||
|
): DuxActions<{ actions: L; subduxes: S }>;
|
||||||
if (actionType) {
|
|
||||||
return createAction(actionType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildActions(localActions = {}, subduxes = {}) {
|
export function buildActions(localActions = {}, subduxes = {}) {
|
||||||
localActions = R.mapValues(localActions, expandAction);
|
localActions = D.mapWithKey(localActions, (key, value) =>
|
||||||
|
expandAction(value, String(key)),
|
||||||
|
);
|
||||||
|
|
||||||
let actions: Record<string, string> = {};
|
let actions: Record<string, string> = {};
|
||||||
|
|
||||||
@ -90,18 +41,27 @@ 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 R.mergeAll([
|
return [
|
||||||
localActions,
|
localActions,
|
||||||
...Object.values(subduxes).map(R.pathOr<any, any>(['actions'], {})),
|
...D.values(subduxes).map((s: any) => s.actions ?? {}),
|
||||||
]) as any;
|
].reduce(D.merge);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,67 +1,30 @@
|
|||||||
import { createAction } from '@reduxjs/toolkit';
|
|
||||||
import { expectTypeOf } from 'expect-type';
|
|
||||||
import { test, expect } from 'vitest';
|
import { test, expect } from 'vitest';
|
||||||
import Updux from './Updux.js';
|
import Updux, { createAction, withPayload } from './index.js';
|
||||||
|
|
||||||
|
const incr = createAction('incr', withPayload<number>());
|
||||||
|
|
||||||
const dux = new Updux({
|
const dux = new Updux({
|
||||||
initialState: 'a',
|
initialState: 1,
|
||||||
actions: {
|
// selectors: {
|
||||||
action1: 0,
|
// double: (x: string) => x + x,
|
||||||
},
|
// },
|
||||||
selectors: {
|
}).addMutation(incr, (i, action) => state => {
|
||||||
double: (x: string) => x + x,
|
expectTypeOf(i).toEqualTypeOf<number>();
|
||||||
},
|
expectTypeOf(state).toEqualTypeOf<number>();
|
||||||
|
expectTypeOf(action).toEqualTypeOf<{ type: 'incr', payload: number; }>();
|
||||||
|
return state + i
|
||||||
});
|
});
|
||||||
|
|
||||||
dux.addMutation(dux.actions.action1, () => (state) => 'mutation1');
|
suite.only('store dispatch actions', () => {
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
expect(store.getState()).toEqual('a');
|
test('dispatch actions', () => {
|
||||||
|
expect(store.dispatch.incr).toBeTypeOf('function');
|
||||||
expectTypeOf(store.getState()).toMatchTypeOf<string>();
|
expectTypeOf(store.dispatch.incr).toMatchTypeOf<Function>();
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
subdux1.addMutation(incr, () => (state) => state + 1);
|
test('reducer does something', () => {
|
||||||
|
store.dispatch.incr(7);
|
||||||
const dux = new Updux({
|
expect(store.getState()).toEqual(8);
|
||||||
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 });
|
|
||||||
});
|
});
|
||||||
|
66
src/createStore.test.ts.2024-07-29
Normal file
66
src/createStore.test.ts.2024-07-29
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { expectTypeOf } from 'expect-type';
|
||||||
|
import Updux from './Updux.js';
|
||||||
|
|
||||||
|
const dux = new Updux({
|
||||||
|
initialState: 'a',
|
||||||
|
actions: {
|
||||||
|
action1: 0,
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
double: (x: string) => x + x,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
expect(store.getState()).toEqual('a');
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
subdux1.addMutation(incr, () => (state) => state + 1);
|
||||||
|
|
||||||
|
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 });
|
||||||
|
});
|
@ -1,24 +1,3 @@
|
|||||||
import { AnyAction, Dispatch, MiddlewareAPI } from '@reduxjs/toolkit';
|
|
||||||
import { DuxActions } from './actions.js';
|
|
||||||
import { DuxSelectors } from './selectors.js';
|
|
||||||
import { DuxState } from './types.js';
|
|
||||||
|
|
||||||
type XSel<R> = R extends Function ? R : () => R;
|
|
||||||
type CurriedSelector<S> = S extends (...args: any) => infer R ? XSel<R> : never;
|
|
||||||
|
|
||||||
type CurriedSelectors<S> = {
|
|
||||||
[key in keyof S]: CurriedSelector<S[key]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AugmentedMiddlewareAPI<D> = MiddlewareAPI<
|
|
||||||
Dispatch<AnyAction>,
|
|
||||||
DuxState<D>
|
|
||||||
> & {
|
|
||||||
dispatch: DuxActions<D>;
|
|
||||||
getState: CurriedSelectors<DuxSelectors<D>>;
|
|
||||||
actions: DuxActions<D>;
|
|
||||||
selectors: DuxSelectors<D>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function augmentGetState(originalGetState, selectors) {
|
export function augmentGetState(originalGetState, selectors) {
|
||||||
const getState = () => originalGetState();
|
const getState = () => originalGetState();
|
||||||
@ -31,3 +10,14 @@ export function augmentGetState(originalGetState, selectors) {
|
|||||||
}
|
}
|
||||||
return getState;
|
return getState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function augmentDispatch(dispatch, actions) {
|
||||||
|
for (const a in actions) {
|
||||||
|
dispatch[a] = (...args) => {
|
||||||
|
const action = actions[a](...args);
|
||||||
|
dispatch(action);
|
||||||
|
return action;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return dispatch;
|
||||||
|
}
|
||||||
|
@ -2,12 +2,41 @@ import { test, expect } from 'vitest';
|
|||||||
import { expectTypeOf } from 'expect-type';
|
import { expectTypeOf } from 'expect-type';
|
||||||
import Updux from './Updux.js';
|
import Updux from './Updux.js';
|
||||||
import { buildEffectsMiddleware } from './effects.js';
|
import { buildEffectsMiddleware } from './effects.js';
|
||||||
import { createAction } from './index.js';
|
import { createAction, withPayload } from './index.js';
|
||||||
|
import { AnyAction, Dispatch } from '@reduxjs/toolkit';
|
||||||
|
import { AugmentedMiddlewareAPI } from './types.js';
|
||||||
|
|
||||||
test('addEffect', () => {
|
test('addEffect signatures', () => {
|
||||||
const dux = new Updux({});
|
const someAction = createAction('someAction', withPayload<number>());
|
||||||
|
const dux = new Updux({
|
||||||
|
actions: {
|
||||||
|
someAction,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
dux.addEffect((api) => (next) => (action) => {});
|
dux.addEffect((api) => (next) => (action) => {
|
||||||
|
expectTypeOf(action).toMatchTypeOf<AnyAction>();
|
||||||
|
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
|
||||||
|
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
dux.addEffect('someAction', (api) => (next) => (action) => {
|
||||||
|
expectTypeOf(action).toMatchTypeOf<{ type: 'someAction' }>();
|
||||||
|
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
|
||||||
|
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
dux.addEffect(someAction, (api) => (next) => (action) => {
|
||||||
|
expectTypeOf(action).toMatchTypeOf<{ type: 'someAction' }>();
|
||||||
|
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
|
||||||
|
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
|
||||||
|
});
|
||||||
|
|
||||||
|
dux.addEffect((action) => action?.payload === 3, (api) => (next) => (action) => {
|
||||||
|
expectTypeOf(action).toMatchTypeOf<AnyAction>();
|
||||||
|
expectTypeOf(next).toMatchTypeOf<Dispatch<AnyAction>>();
|
||||||
|
expectTypeOf(api).toMatchTypeOf<AugmentedMiddlewareAPI<{}>>();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildEffectsMiddleware', () => {
|
test('buildEffectsMiddleware', () => {
|
||||||
@ -48,7 +77,7 @@ test('buildEffectsMiddleware', () => {
|
|||||||
|
|
||||||
expect(seen).toEqual(0);
|
expect(seen).toEqual(0);
|
||||||
const dispatch = vi.fn();
|
const dispatch = vi.fn();
|
||||||
mw({ getState: () => 'the state', dispatch })(() => {})({
|
mw({ getState: () => 'the state', dispatch })(() => { })({
|
||||||
type: 'noop',
|
type: 'noop',
|
||||||
});
|
});
|
||||||
expect(seen).toEqual(1);
|
expect(seen).toEqual(1);
|
||||||
@ -61,7 +90,7 @@ test('basic', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
foo: 0,
|
foo: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -87,7 +116,7 @@ test('basic', () => {
|
|||||||
test('subdux', () => {
|
test('subdux', () => {
|
||||||
const bar = new Updux({
|
const bar = new Updux({
|
||||||
initialState: 'bar state',
|
initialState: 'bar state',
|
||||||
actions: { foo: 0 },
|
actions: { foo: null },
|
||||||
});
|
});
|
||||||
|
|
||||||
let seen = 0;
|
let seen = 0;
|
||||||
@ -144,8 +173,8 @@ test('addEffect with actionCreator', () => {
|
|||||||
test('addEffect with function', () => {
|
test('addEffect with function', () => {
|
||||||
const dux = new Updux({
|
const dux = new Updux({
|
||||||
actions: {
|
actions: {
|
||||||
foo: () => {},
|
foo: () => { },
|
||||||
bar: () => {},
|
bar: () => { },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { AnyAction } from '@reduxjs/toolkit';
|
import { AnyAction } from '@reduxjs/toolkit';
|
||||||
import { Dispatch } from '@reduxjs/toolkit';
|
import { Dispatch } from '@reduxjs/toolkit';
|
||||||
import { MiddlewareAPI } from '@reduxjs/toolkit';
|
import { MiddlewareAPI } from '@reduxjs/toolkit';
|
||||||
import { AugmentedMiddlewareAPI, augmentGetState } from './createStore.js';
|
import { augmentGetState } from './createStore.js';
|
||||||
|
import { AugmentedMiddlewareAPI } from './types.js';
|
||||||
|
|
||||||
//const composeMw = (mws) => (api) => (originalNext) =>
|
//const composeMw = (mws) => (api) => (originalNext) =>
|
||||||
// mws.reduceRight((next, mw) => mw(api)(next), originalNext);
|
// mws.reduceRight((next, mw) => mw(api)(next), originalNext);
|
||||||
|
|
||||||
export interface EffectMiddleware<D> {
|
export interface EffectMiddleware<D, A = AnyAction> {
|
||||||
(api: AugmentedMiddlewareAPI<D>): (
|
(api: AugmentedMiddlewareAPI<D>): (
|
||||||
next: Dispatch<AnyAction>,
|
next: Dispatch<AnyAction>,
|
||||||
) => (action: AnyAction) => any;
|
) => (action: A) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildEffects(localEffects, subduxes = {}) {
|
export function buildEffects(localEffects, subduxes = {}) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export { withPayload } from './actions.js';
|
|
||||||
import Updux from './Updux.js';
|
import Updux from './Updux.js';
|
||||||
|
|
||||||
|
export { withPayload } from './actions.js';
|
||||||
export { createAction } from '@reduxjs/toolkit';
|
export { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export default Updux;
|
export default Updux;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user