lotsa work
This commit is contained in:
parent
edb6716c9c
commit
66c2b162db
2
.gitignore
vendored
2
.gitignore
vendored
@ -11,3 +11,5 @@ GPUCache/
|
||||
updux-2.0.0.tgz
|
||||
pnpm-lock.yaml
|
||||
updux-5.0.0.tgz
|
||||
.envrc
|
||||
.task
|
||||
|
11
TODO
11
TODO
@ -1,2 +1,9 @@
|
||||
- documentation generator (mkdocs + jsdoc-to-markdown)
|
||||
- createStore
|
||||
* documentation action + mutation <<<
|
||||
* 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 weld -
|
||||
|
||||
test: vitest run src docs/*.ts
|
||||
test: vitest run src
|
||||
|
||||
test:dev: vitest src
|
||||
|
||||
lint:fix:delta:
|
||||
@ -65,6 +66,12 @@ tasks:
|
||||
cmds:
|
||||
- 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
|
||||
|
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
|
||||
}
|
||||
});
|
||||
|
||||
```
|
@ -6,7 +6,7 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Description">
|
||||
<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>
|
||||
<body>
|
||||
<div id="app"></div>
|
@ -1,88 +1,12 @@
|
||||
# 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.
|
||||
|
||||
```
|
||||
--8<- "docs/tutorial-1.test.ts: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:
|
||||
|
||||
```
|
||||
--8<- "docs/tutorial-1.test.ts:tut2"
|
||||
```
|
||||
|
||||
## Add actions
|
||||
|
||||
This is all good, but very static. Let's add some actions!
|
||||
|
||||
```
|
||||
--8<- "docs/tutorial-actions.test.ts: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.
|
||||
|
||||
```
|
||||
--8<- "docs/tutorial-actions.test.ts:actions2"
|
||||
```
|
||||
|
||||
### Adding a mutation
|
||||
|
||||
Mutations are the reducing functions associated to actions. They
|
||||
are defined via the `addMutation` method.
|
||||
|
||||
```
|
||||
--8<- "docs/tutorial-actions.test.ts: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.
|
||||
|
||||
```
|
||||
--8<- "docs/tutorial-effects.test.ts: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.
|
||||
|
||||
```
|
||||
--8<- "docs/tutorial-selectors.test.ts:sel1"
|
||||
|
||||
|
||||
```
|
||||
|
||||
## Subduxes
|
||||
@ -96,7 +20,7 @@ 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"
|
||||
```title="todos-monolith.ts"
|
||||
--8<- "docs/tutorial-monolith.test.ts:mono"
|
||||
```
|
||||
|
||||
@ -106,25 +30,25 @@ create upduxes for each of those.
|
||||
|
||||
### NextId dux
|
||||
|
||||
``` js title="nextId.ts"
|
||||
```js title="nextId.ts"
|
||||
--8<- "docs/nextId.ts:dux"
|
||||
```
|
||||
|
||||
### Todo updux
|
||||
|
||||
``` title="todo.ts"
|
||||
```title="todo.ts"
|
||||
--8<- "docs/todo.ts"
|
||||
```
|
||||
|
||||
### Todos updux
|
||||
|
||||
``` title="todos.ts"
|
||||
```title="todos.ts"
|
||||
--8<- "docs/todos.ts"
|
||||
```
|
||||
|
||||
### Main store
|
||||
|
||||
``` title="todoList.ts"
|
||||
```title="todoList.ts"
|
||||
--8<- "docs/todoList.ts"
|
||||
```
|
||||
|
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
|
||||
[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
|
||||
[![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build)
|
||||
|
||||
```
|
||||
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);
|
||||
npm create astro@latest -- --template starlight
|
||||
```
|
||||
|
||||
# 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/).
|
||||
Right now the best way to understand the whole thing is to go
|
||||
through the [tutorial](https://yanick.github.io/updux/#/tutorial)
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
## Exporting upduxes
|
||||
## 🚀 Project Structure
|
||||
|
||||
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:
|
||||
Inside of your Astro + Starlight project, you'll see the following folders and files:
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
|
||||
const updux = new Updux({ ... });
|
||||
|
||||
export default updux;
|
||||
.
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── assets/
|
||||
│ ├── 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.
|
||||
|
||||
```
|
||||
import Updux from 'updux';
|
||||
import foo from './foo'; // foo is an Updux
|
||||
import bar from './bar'; // bar is an Updux as well
|
||||
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
|
||||
|
||||
const updux = new Updux({
|
||||
subduxes: {
|
||||
foo, bar
|
||||
}
|
||||
});
|
||||
```
|
||||
Static assets, like favicons, can be placed in the `public/` directory.
|
||||
|
||||
Or if you want to use it:
|
||||
## 🧞 Commands
|
||||
|
||||
```
|
||||
import updux from './myUpdux';
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
const {
|
||||
reducer,
|
||||
actions: { doTheThing },
|
||||
createStore,
|
||||
middleware,
|
||||
} = updux;
|
||||
```
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `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
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
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).
|
||||
|
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 @@
|
||||
/// --8<-- [start: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;
|
||||
|
||||
/// --8<-- [end: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';
|
||||
|
||||
/// --8<-- [start:tut1]
|
||||
import Updux from 'updux';
|
||||
|
||||
const todosDux = new Updux({
|
||||
initialState: {
|
||||
nextId: 1,
|
||||
todos: [],
|
||||
}
|
||||
});
|
||||
|
||||
/// ---8<-- [end:tut1]
|
||||
|
||||
/// ---8<-- [start:tut2]
|
||||
const store = todosDux.createStore();
|
||||
|
||||
store.getState(); // { nextId: 1, todos: [] }
|
||||
/// ---8<-- [end: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);
|
||||
|
||||
|
||||
|
||||
});
|
@ -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/
|
@ -1,7 +1,8 @@
|
||||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@yanick/updeep-remeda": "^2.2.0",
|
||||
"@mobily/ts-belt": "^3.13.1",
|
||||
"@yanick/updeep-remeda": "^2.3.1",
|
||||
"ajv": "^8.12.0",
|
||||
"expect-type": "^0.16.0",
|
||||
"immer": "^9.0.15",
|
||||
@ -43,7 +44,7 @@
|
||||
"devDependencies": {
|
||||
"@reduxjs/toolkit": "==2.0.0-alpha.5 ",
|
||||
"@vitest/browser": "^0.23.1",
|
||||
"@vitest/ui": "^0.23.1",
|
||||
"@vitest/ui": "^2.0.5",
|
||||
"docsify": "^4.13.1",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-no-only-tests": "^3.0.0",
|
||||
@ -56,6 +57,6 @@
|
||||
"typedoc-plugin-markdown": "^3.17.1",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.2.1",
|
||||
"vitest": "0.23.1"
|
||||
"vitest": "2.0.4"
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
Action,
|
||||
ActionCreator,
|
||||
AnyAction,
|
||||
configureStore,
|
||||
EnhancedStore,
|
||||
Action,
|
||||
PayloadAction,
|
||||
AnyAction,
|
||||
ActionCreatorWithPreparedPayload,
|
||||
} 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';
|
||||
AugmentedMiddlewareAPI,
|
||||
DuxActions,
|
||||
DuxConfig,
|
||||
DuxReaction,
|
||||
DuxSelectors,
|
||||
DuxState,
|
||||
Mutation,
|
||||
} from './types.js';
|
||||
import baseMoize from 'moize/mjs/index.mjs';
|
||||
import { buildInitialState } from './initialState.js';
|
||||
import { buildActions } from './actions.js';
|
||||
import { D } from '@mobily/ts-belt';
|
||||
import { buildReducer } from './reducer.js';
|
||||
import { 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> = (
|
||||
payload: A extends {
|
||||
payload: infer P;
|
||||
}
|
||||
? P
|
||||
: undefined,
|
||||
action: A,
|
||||
) => (state: S) => S | void;
|
||||
type CreateStoreOptions<D> = Partial<{
|
||||
preloadedState: DuxState<D>;
|
||||
}>;
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
interface ActionCreator<T extends string, P, A extends Array<any>> {
|
||||
type: T;
|
||||
match: (
|
||||
action: Action<unknown>,
|
||||
) => action is PayloadAction<P, T, never, never>;
|
||||
(...args: A): PayloadAction<P, T, never, never>;
|
||||
}
|
||||
|
||||
const moize = (func) => baseMoize(func, { maxSize: 1 });
|
||||
|
||||
/**
|
||||
* @description main Updux class
|
||||
*/
|
||||
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) {
|
||||
// just to warn at creation time if config has issues
|
||||
this.actions;
|
||||
if (duxConfig.subduxes)
|
||||
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> {
|
||||
return this.memoInitialState(
|
||||
return this.#memoInitialState(
|
||||
this.duxConfig.initialState,
|
||||
this.duxConfig.subduxes,
|
||||
this.#subduxes,
|
||||
);
|
||||
}
|
||||
|
||||
get subduxes() {
|
||||
return this.#subduxes;
|
||||
}
|
||||
|
||||
|
||||
get actions(): DuxActions<D> {
|
||||
return this.memoBuildActions(
|
||||
this.duxConfig.actions,
|
||||
this.duxConfig.subduxes,
|
||||
);
|
||||
}
|
||||
|
||||
toDux() {
|
||||
return {
|
||||
initialState: this.initialState,
|
||||
actions: this.actions,
|
||||
reducer: this.reducer,
|
||||
effects: this.effects,
|
||||
reactions: this.reactions,
|
||||
selectors: this.selectors,
|
||||
upreducer: this.upreducer,
|
||||
schema: this.schema,
|
||||
};
|
||||
}
|
||||
get asDux() {
|
||||
return this.toDux();
|
||||
}
|
||||
|
||||
get 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;
|
||||
return this.#actions as any;
|
||||
}
|
||||
|
||||
createStore(
|
||||
options: Partial<{
|
||||
preloadedState: DuxState<D>;
|
||||
validate: boolean;
|
||||
buildMiddleware: (middleware: any[]) => any
|
||||
}> = {},
|
||||
options: CreateStoreOptions<D> = {},
|
||||
): EnhancedStore<DuxState<D>> & AugmentedMiddlewareAPI<D> {
|
||||
const preloadedState = options.preloadedState;
|
||||
|
||||
@ -204,32 +121,19 @@ export default class Updux<D extends DuxConfig> {
|
||||
|
||||
let middleware = [effects];
|
||||
|
||||
if (options.validate) {
|
||||
middleware.unshift(
|
||||
buildValidateMiddleware(this.schema)
|
||||
)
|
||||
}
|
||||
|
||||
if (options.buildMiddleware)
|
||||
middleware = options.buildMiddleware(middleware);
|
||||
|
||||
const store = configureStore({
|
||||
reducer: this.reducer,
|
||||
const store: any = configureStore({
|
||||
preloadedState,
|
||||
reducer: this.reducer,
|
||||
middleware,
|
||||
});
|
||||
|
||||
const dispatch: any = store.dispatch;
|
||||
for (const a in this.actions) {
|
||||
dispatch[a] = (...args) => {
|
||||
const action = (this.actions as any)[a](...args);
|
||||
dispatch(action);
|
||||
return action;
|
||||
};
|
||||
}
|
||||
store.dispatch = augmentDispatch(store.dispatch, this.actions);
|
||||
|
||||
store.getState = augmentGetState(store.getState, this.selectors);
|
||||
|
||||
store.actions = this.actions;
|
||||
store.selectors = this.selectors;
|
||||
|
||||
for (const reaction of this.reactions) {
|
||||
let unsub;
|
||||
const r = reaction(store);
|
||||
@ -237,32 +141,110 @@ export default class Updux<D extends DuxConfig> {
|
||||
unsub = store.subscribe(() => r(unsub));
|
||||
}
|
||||
|
||||
(store as any).actions = this.actions;
|
||||
(store as any).selectors = this.selectors;
|
||||
|
||||
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(
|
||||
actionType: keyof DuxActions<D>,
|
||||
effect: EffectMiddleware<D>,
|
||||
addMutation<A extends keyof DuxActions<D>>(
|
||||
actionType: A,
|
||||
mutation: Mutation<
|
||||
DuxActions<D>[A] extends (...args: any[]) => infer R ? R : AnyAction,
|
||||
DuxState<D>
|
||||
>,
|
||||
terminal?: boolean,
|
||||
): Updux<D>;
|
||||
addEffect(
|
||||
actionCreator: { match: (action: any) => boolean },
|
||||
effect: EffectMiddleware<D>,
|
||||
addMutation<AC extends ActionCreatorWithPreparedPayload<any, any, string, never, never>>(
|
||||
actionCreator: AC,
|
||||
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>;
|
||||
addEffect(
|
||||
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 {
|
||||
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(
|
||||
reaction// :DuxReaction<D>
|
||||
reaction: DuxReaction<D>
|
||||
) {
|
||||
let previous: any;
|
||||
|
||||
const memoized = (api: any) => {
|
||||
api = augmentMiddlewareApi(api, this.actions, this.selectors);
|
||||
const r = reaction(api);
|
||||
return (unsub: () => void) => {
|
||||
const state = api.getState();
|
||||
if (state === previous) return;
|
||||
|
||||
const rMemoized = (localState, unsub) => {
|
||||
let p = previous;
|
||||
previous = state;
|
||||
r(state, p, unsub);
|
||||
previous = localState;
|
||||
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() {
|
||||
return [
|
||||
...this.#reactions,
|
||||
...(Object.entries(this.duxConfig.subduxes ?? {}) as any).flatMap(
|
||||
([slice, { reactions }]) =>
|
||||
reactions.map(
|
||||
(r) => (api, unsub) =>
|
||||
r(
|
||||
{
|
||||
...api,
|
||||
getState: () => api.getState()[slice],
|
||||
},
|
||||
unsub,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
return this.#memoBuildReactions(this.#reactions, this.#subduxes);
|
||||
}
|
||||
|
||||
}
|
||||
|
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 { createAction, withPayload, expandAction } from './actions.js';
|
||||
import type { ExpandedAction } from './actions.js';
|
||||
import { expectTypeOf } from 'expect-type';
|
||||
import {
|
||||
ActionCreatorWithoutPayload,
|
||||
ActionCreatorWithPreparedPayload,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
|
||||
import { buildActions, createAction, withPayload } from './actions.js';
|
||||
import Updux from './index.js';
|
||||
|
||||
test('basic action', () => {
|
||||
const foo = createAction(
|
||||
@ -28,187 +23,91 @@ test('basic action', () => {
|
||||
}>();
|
||||
});
|
||||
|
||||
test('Updux config accepts actions', () => {
|
||||
const foo = new Updux({
|
||||
actions: {
|
||||
one: createAction(
|
||||
'one',
|
||||
withPayload((x) => ({ x })),
|
||||
),
|
||||
two: createAction(
|
||||
'two',
|
||||
withPayload((x) => x),
|
||||
),
|
||||
},
|
||||
test('withPayload, just the type', () => {
|
||||
const foo = createAction('foo', withPayload<string>());
|
||||
|
||||
expect(foo('bar')).toEqual({
|
||||
type: 'foo',
|
||||
payload: 'bar',
|
||||
});
|
||||
|
||||
expect(Object.keys(foo.actions)).toHaveLength(2);
|
||||
expectTypeOf(foo).parameters.toMatchTypeOf<[string]>();
|
||||
|
||||
expect(foo.actions.one).toBeTypeOf('function');
|
||||
expect(foo.actions.one('potato')).toEqual({
|
||||
expectTypeOf(foo).returns.toMatchTypeOf<{
|
||||
type: 'foo';
|
||||
payload: string;
|
||||
}>();
|
||||
});
|
||||
|
||||
test('buildActions', () => {
|
||||
const actions = buildActions(
|
||||
{
|
||||
one: createAction('one'), two: (x: string) => x,
|
||||
withoutValue: null,
|
||||
},
|
||||
{ a: { actions: buildActions({ three: () => 3 }) } },
|
||||
);
|
||||
|
||||
expect(actions.one()).toEqual({
|
||||
type: 'one',
|
||||
payload: {
|
||||
x: 'potato',
|
||||
},
|
||||
});
|
||||
|
||||
expectTypeOf(foo.actions.one).toMatchTypeOf<
|
||||
ActionCreatorWithPreparedPayload<any, any, any, any>
|
||||
>();
|
||||
expectTypeOf(actions.one()).toMatchTypeOf<{
|
||||
type: 'one';
|
||||
}>();
|
||||
|
||||
expect(actions.two('potato')).toEqual({ type: 'two', payload: 'potato' });
|
||||
|
||||
expectTypeOf(actions.two('potato')).toMatchTypeOf<{
|
||||
type: 'two';
|
||||
payload: string;
|
||||
}>();
|
||||
|
||||
expect(actions.three()).toEqual({ type: 'three', payload: 3 });
|
||||
|
||||
expectTypeOf(actions.three()).toMatchTypeOf<{
|
||||
type: 'three';
|
||||
payload: number;
|
||||
}>();
|
||||
|
||||
expect(actions.withoutValue()).toEqual({ type: 'withoutValue' });
|
||||
|
||||
expectTypeOf(actions.withoutValue()).toMatchTypeOf<{
|
||||
type: 'withoutValue';
|
||||
}>();
|
||||
});
|
||||
|
||||
describe('expandAction', () => {
|
||||
test('as-is', () => {
|
||||
const result = expandAction(
|
||||
createAction('foo', withPayload<boolean>()),
|
||||
);
|
||||
|
||||
expectTypeOf(result).toMatchTypeOf<
|
||||
(input: boolean) => { type: 'foo'; payload: boolean }
|
||||
>();
|
||||
|
||||
expect(result(true)).toMatchObject({
|
||||
type: 'foo',
|
||||
payload: true,
|
||||
});
|
||||
});
|
||||
test('0', () => {
|
||||
const result = expandAction(0, 'foo');
|
||||
|
||||
expectTypeOf(result).toMatchTypeOf<() => { type: 'foo' }>();
|
||||
|
||||
expect(result()).toMatchObject({
|
||||
type: 'foo',
|
||||
});
|
||||
});
|
||||
test('function', () => {
|
||||
const result = expandAction((size: number) => ({ size }), 'foo');
|
||||
|
||||
expectTypeOf(result).toMatchTypeOf<
|
||||
() => { type: 'foo'; payload: { size: number } }
|
||||
>();
|
||||
|
||||
expect(result(12)).toMatchObject({
|
||||
type: 'foo',
|
||||
payload: {
|
||||
size: 12,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('action types', () => {
|
||||
const action1 = createAction('a1', withPayload<number>());
|
||||
expectTypeOf(
|
||||
true as any as ExpandedAction<typeof action1, 'a1'>,
|
||||
).toMatchTypeOf<
|
||||
ActionCreatorWithPreparedPayload<
|
||||
[input: number],
|
||||
number,
|
||||
'a1',
|
||||
never,
|
||||
never
|
||||
>
|
||||
>();
|
||||
const action2 = (input: boolean) => input;
|
||||
let typed2: ExpandedAction<typeof action2, 'a2'>;
|
||||
expectTypeOf(typed2).toMatchTypeOf<
|
||||
ActionCreatorWithPreparedPayload<
|
||||
[input: boolean],
|
||||
boolean,
|
||||
'a2',
|
||||
never,
|
||||
never
|
||||
>
|
||||
>();
|
||||
let typed3: ExpandedAction<boolean, 'a3'>;
|
||||
expectTypeOf(typed3).toMatchTypeOf<ActionCreatorWithoutPayload<'a3'>>();
|
||||
});
|
||||
|
||||
test('action definition shortcut', () => {
|
||||
const foo = new Updux({
|
||||
describe('Updux interactions', () => {
|
||||
const dux = new Updux({
|
||||
initialState: { a: 3 },
|
||||
actions: {
|
||||
foo: 0,
|
||||
bar: (x: number) => ({ x }),
|
||||
baz: createAction('baz', withPayload<boolean>()),
|
||||
},
|
||||
});
|
||||
|
||||
expect(foo.actions.foo()).toEqual({ type: 'foo', payload: undefined });
|
||||
expect(foo.actions.baz(false)).toEqual({
|
||||
type: 'baz',
|
||||
payload: false,
|
||||
});
|
||||
expect(foo.actions.bar(2)).toEqual({
|
||||
type: 'bar',
|
||||
payload: { x: 2 },
|
||||
});
|
||||
|
||||
expectTypeOf(foo.actions.foo).toMatchTypeOf<ActionCreatorWithoutPayload<'foo'>>();
|
||||
});
|
||||
|
||||
test('subduxes actions', () => {
|
||||
const bar = createAction<number>('bar');
|
||||
const baz = createAction('baz');
|
||||
|
||||
const foo = new Updux({
|
||||
actions: {
|
||||
bar,
|
||||
add: (x: number) => x,
|
||||
withNull: null,
|
||||
},
|
||||
subduxes: {
|
||||
beta: {
|
||||
subdux1: new Updux({
|
||||
actions: {
|
||||
baz,
|
||||
},
|
||||
},
|
||||
// to check if we can deal with empty actions
|
||||
gamma: {},
|
||||
},
|
||||
fromSubdux: (x: string) => x
|
||||
}
|
||||
}),
|
||||
subdux2: {
|
||||
actions: {
|
||||
ohmy: () => { }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
expect(foo.actions).toHaveProperty('bar');
|
||||
expect(foo.actions).toHaveProperty('baz');
|
||||
test('actions getter', () => {
|
||||
expect(dux.actions.add).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(foo.actions.bar(2)).toHaveProperty('type', 'bar');
|
||||
expect(foo.actions.baz()).toHaveProperty('type', 'baz');
|
||||
expectTypeOf(dux.actions.withNull).toMatchTypeOf<
|
||||
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 {
|
||||
ActionCreator,
|
||||
ActionCreatorWithoutPayload,
|
||||
createAction,
|
||||
} from '@reduxjs/toolkit';
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
import { D } from '@mobily/ts-belt';
|
||||
import { DuxActions, DuxConfig, Subduxes } from './types.js';
|
||||
|
||||
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, A extends any[]>(
|
||||
@ -60,20 +14,17 @@ export function withPayload(prepare = (input) => input) {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildActions<L extends DuxConfig['actions']>(
|
||||
localActions: L,
|
||||
): DuxActions<{ actions: L }>;
|
||||
export function buildActions<L extends DuxActions<any>, S extends Subduxes>(
|
||||
localActions: L,
|
||||
subduxes: S,
|
||||
): DuxActions<{ actions: L; subduxes: S }>;
|
||||
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> = {};
|
||||
|
||||
@ -90,18 +41,27 @@ export function buildActions(localActions = {}, subduxes = {}) {
|
||||
}
|
||||
actions[a] = slice;
|
||||
}
|
||||
}
|
||||
|
||||
for (const a in localActions) {
|
||||
if (actions[a]) {
|
||||
throw new Error(
|
||||
`action '${a}' defined both locally and in subdux '${actions[a]}'`,
|
||||
);
|
||||
for (const a in localActions) {
|
||||
if (actions[a]) {
|
||||
throw new Error(
|
||||
`action '${a}' defined both locally and in subdux '${actions[a]}'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return R.mergeAll([
|
||||
return [
|
||||
localActions,
|
||||
...Object.values(subduxes).map(R.pathOr<any, any>(['actions'], {})),
|
||||
]) as any;
|
||||
...D.values(subduxes).map((s: any) => s.actions ?? {}),
|
||||
].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 Updux from './Updux.js';
|
||||
import Updux, { createAction, withPayload } from './index.js';
|
||||
|
||||
const incr = createAction('incr', withPayload<number>());
|
||||
|
||||
const dux = new Updux({
|
||||
initialState: 'a',
|
||||
actions: {
|
||||
action1: 0,
|
||||
},
|
||||
selectors: {
|
||||
double: (x: string) => x + x,
|
||||
},
|
||||
initialState: 1,
|
||||
// selectors: {
|
||||
// double: (x: string) => x + x,
|
||||
// },
|
||||
}).addMutation(incr, (i, action) => state => {
|
||||
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');
|
||||
|
||||
test('createStore', () => {
|
||||
expect(dux.createStore().getState()).toEqual('a');
|
||||
expect(dux.createStore({ preloadedState: 'b' }).getState()).toEqual('b');
|
||||
});
|
||||
|
||||
test('augmentGetState', () => {
|
||||
suite.only('store dispatch actions', () => {
|
||||
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,
|
||||
test('dispatch actions', () => {
|
||||
expect(store.dispatch.incr).toBeTypeOf('function');
|
||||
expectTypeOf(store.dispatch.incr).toMatchTypeOf<Function>();
|
||||
});
|
||||
|
||||
subdux1.addMutation(incr, () => (state) => state + 1);
|
||||
|
||||
const dux = new Updux({
|
||||
subduxes: {
|
||||
subdux1: subdux1.asDux,
|
||||
},
|
||||
test('reducer does something', () => {
|
||||
store.dispatch.incr(7);
|
||||
expect(store.getState()).toEqual(8);
|
||||
});
|
||||
|
||||
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) {
|
||||
const getState = () => originalGetState();
|
||||
@ -31,3 +10,14 @@ export function augmentGetState(originalGetState, selectors) {
|
||||
}
|
||||
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 Updux from './Updux.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', () => {
|
||||
const dux = new Updux({});
|
||||
test('addEffect signatures', () => {
|
||||
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', () => {
|
||||
@ -48,7 +77,7 @@ test('buildEffectsMiddleware', () => {
|
||||
|
||||
expect(seen).toEqual(0);
|
||||
const dispatch = vi.fn();
|
||||
mw({ getState: () => 'the state', dispatch })(() => {})({
|
||||
mw({ getState: () => 'the state', dispatch })(() => { })({
|
||||
type: 'noop',
|
||||
});
|
||||
expect(seen).toEqual(1);
|
||||
@ -61,7 +90,7 @@ test('basic', () => {
|
||||
loaded: true,
|
||||
},
|
||||
actions: {
|
||||
foo: 0,
|
||||
foo: null,
|
||||
},
|
||||
});
|
||||
|
||||
@ -87,7 +116,7 @@ test('basic', () => {
|
||||
test('subdux', () => {
|
||||
const bar = new Updux({
|
||||
initialState: 'bar state',
|
||||
actions: { foo: 0 },
|
||||
actions: { foo: null },
|
||||
});
|
||||
|
||||
let seen = 0;
|
||||
@ -144,8 +173,8 @@ test('addEffect with actionCreator', () => {
|
||||
test('addEffect with function', () => {
|
||||
const dux = new Updux({
|
||||
actions: {
|
||||
foo: () => {},
|
||||
bar: () => {},
|
||||
foo: () => { },
|
||||
bar: () => { },
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { Dispatch } 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) =>
|
||||
// mws.reduceRight((next, mw) => mw(api)(next), originalNext);
|
||||
|
||||
export interface EffectMiddleware<D> {
|
||||
export interface EffectMiddleware<D, A = AnyAction> {
|
||||
(api: AugmentedMiddlewareAPI<D>): (
|
||||
next: Dispatch<AnyAction>,
|
||||
) => (action: AnyAction) => any;
|
||||
) => (action: A) => any;
|
||||
}
|
||||
|
||||
export function buildEffects(localEffects, subduxes = {}) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
export { withPayload } from './actions.js';
|
||||
import Updux from './Updux.js';
|
||||
|
||||
export { withPayload } from './actions.js';
|
||||
export { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export default Updux;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { expectTypeOf } from 'expect-type';
|
||||
import { buildInitialState } from './initialState.js';
|
||||
import Updux from './Updux.js';
|
||||
|
||||
@ -14,10 +13,12 @@ test('default', () => {
|
||||
test('number', () => {
|
||||
const dux = new Updux({ initialState: 3 });
|
||||
|
||||
expect(dux.initialState).toBeTypeOf('number');
|
||||
expect(dux.initialState).toEqual(3);
|
||||
|
||||
expectTypeOf(dux.initialState).toEqualTypeOf<number>();
|
||||
|
||||
type X = typeof dux.initialState;
|
||||
const f = { initialState: dux.initialState };
|
||||
});
|
||||
|
||||
test('single dux', () => {
|
||||
@ -31,12 +32,12 @@ test('single dux', () => {
|
||||
|
||||
test('no initialState for subdux', () => {
|
||||
const subduxes = {
|
||||
bar: new Updux({}).toDux(),
|
||||
baz: new Updux({ initialState: 'potato' }).toDux(),
|
||||
bar: new Updux({}),
|
||||
baz: new Updux({ initialState: 'potato' }),
|
||||
};
|
||||
const dux = new Updux({
|
||||
subduxes,
|
||||
}).toDux();
|
||||
});
|
||||
|
||||
expectTypeOf(dux.initialState).toEqualTypeOf<{
|
||||
bar: {};
|
||||
@ -69,7 +70,7 @@ const bar = new Updux({ initialState: 123 });
|
||||
const foo = new Updux({
|
||||
initialState: { root: 'abc' },
|
||||
subduxes: {
|
||||
bar: bar.asDux,
|
||||
bar
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import u from '@yanick/updeep-remeda';
|
||||
import * as R from 'remeda';
|
||||
import { D } from '@mobily/ts-belt';
|
||||
|
||||
export function buildInitialState(localInitialState, subduxes) {
|
||||
let state = localInitialState ?? {};
|
||||
@ -9,7 +9,7 @@ export function buildInitialState(localInitialState, subduxes) {
|
||||
throw new Error('root initial state is not an object');
|
||||
}
|
||||
|
||||
state = u(state, R.mapValues(subduxes, R.prop('initialState')));
|
||||
state = u(state, D.map(subduxes, D.prop('initialState')));
|
||||
}
|
||||
|
||||
return state;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user