Skip to content

Commit 9e7fe2c

Browse files
authored
Merge pull request #1647 from reduxjs/feature/action-middleware-package
2 parents 88d929e + c1cd9b3 commit 9e7fe2c

File tree

9 files changed

+710
-3
lines changed

9 files changed

+710
-3
lines changed

.codesandbox/ci.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
"buildCommand": "build:packages",
1111
"packages": [
1212
"packages/toolkit",
13-
"packages/rtk-query-graphql-request-base-query"
13+
"packages/rtk-query-graphql-request-base-query",
14+
"packages/action-listener-middleware"
1415
],
1516
"publishDirectory": {
1617
"@reduxjs/toolkit": "packages/toolkit",
17-
"@rtk-query/graphql-request-base-query": "packages/rtk-query-graphql-request-base-query"
18+
"@rtk-query/graphql-request-base-query": "packages/rtk-query-graphql-request-base-query",
19+
"@rtk-incubator/action-listener-middleware": "packages/action-listener-middleware"
1820
}
1921
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"test": "yarn test:packages",
4646
"build:examples": "yarn workspaces foreach --include '@reduxjs/*' --include '@examples-query-react/*' -vtp run build",
4747
"build:docs": "yarn workspace website run build",
48-
"build:packages": "yarn workspaces foreach --include '@reduxjs/*' --include '@rtk-query/*' --topological-dev run build",
48+
"build:packages": "yarn workspaces foreach --include '@reduxjs/*' --include '@rtk-query/*' --include '@rtk-incubator/*' --topological-dev run build",
4949
"test:packages": "yarn workspaces foreach --include '@reduxjs/*' --include '@rtk-query/*' run test",
5050
"dev:docs": "yarn workspace website run start"
5151
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist
2+
node_modules
3+
yarn-error.log
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Lenz Weber
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# RTK Incubator - Action Listener Middleware
2+
3+
This package provides an experimental callback-based Redux middleware that we hope to include in Redux Toolkit directly in a future release. We're publishing it as a standalone package to allow users to try it out separately and give us feedback on its API design.
4+
5+
This middleware lets you define callbacks that will run in response to specific actions being dispatched. It's intended to be a lightweight alternative to more widely used Redux async middleware like sagas and observables, and similar to thunks in level of complexity and concept.
6+
7+
## Installation
8+
9+
```bash
10+
npm i @rtk-incubator/action-listener-middleware
11+
12+
yarn add @rtk-incubator/action-listener-middleware
13+
```
14+
15+
### Basic Usage
16+
17+
```js
18+
import { configureStore } from '@reduxjs/toolkit'
19+
import { createActionListenerMiddleware } from '@rtk-incubator/action-listener-middleware'
20+
21+
import todosReducer, {
22+
todoAdded,
23+
todoToggled,
24+
todoDeleted,
25+
} from '../features/todos/todosSlice'
26+
27+
// Create the middleware instance
28+
const listenerMiddleware = createActionListenerMiddleware()
29+
30+
// Add one or more listener callbacks for specific actions
31+
listenerMiddleware.addListener(todoAdded, (action, listenerApi) => {
32+
// Run whatever additional side-effect-y logic you want here
33+
const { text } = action.payload
34+
console.log('Todo added: ', text)
35+
36+
if (text === 'Buy milk') {
37+
// Use the listener API methods to dispatch, get state, or unsubscribe the listener
38+
listenerApi.unsubscribe()
39+
}
40+
})
41+
42+
const store = configureStore({
43+
reducer: {
44+
todos: todosReducer,
45+
},
46+
// Add the middleware to the store
47+
middleware: (getDefaultMiddleware) =>
48+
getDefaultMiddleware().concat(listenerMiddleware),
49+
})
50+
```
51+
52+
## Motivation
53+
54+
The Redux community has settled around three primary side effects libraries over time:
55+
56+
- Thunks use basic functions passed to `dispatch`. They let users run arbitrary logic, including dispatching actions and getting state. These are mostly used for basic AJAX requests and logic that needs to read from state before dispatching actions
57+
- Sagas use generator functions and a custom set of "effects" APIs, which are then executed by a middleware. Sagas let users write powerful async logic and workflows that can respond to any dispatched action, including "background thread"-type behavior like infinite loops and cancelation.
58+
- Observables use RxJS obesrvable operators. Observables form pipelines that do arbitrary processing similar to sagas, but with a more functional API style.
59+
60+
All three of those have strengths and weaknesses:
61+
62+
- Thunks are simple to use, but can only run imperative code and have no way to _respond_ to dispatched actions
63+
- Sagas are extremely powerful, but require learning generator functions and the specifics of `redux-saga`'s effects API, and are overkill for many simpler use cases
64+
- Observables are also powerful, but RxJS is its own complex API to learn and they can be hard to debug
65+
66+
If you need to run some code in response to a specific action being dispatched, you _could_ write a custom middleware:
67+
68+
```js
69+
const myMiddleware = (storeAPI) => (next) => (action) => {
70+
if (action.type === 'some/specificAction') {
71+
console.log('Do something useful here')
72+
}
73+
74+
return next(action)
75+
}
76+
```
77+
78+
However, it would be nice to have a more structured API to help abstract this process.
79+
80+
The `createActionListenerMiddleware` API provides that structure.
81+
82+
For more background and debate over the use cases and API design, see the original discussion issue and PR:
83+
84+
- [RTK issue #237: Add an action listener middleware](https://github.com/reduxjs/redux-toolkit/issues/237)
85+
- [RTK PR #547: yet another attempt at an action listener middleware](https://github.com/reduxjs/redux-toolkit/pull/547)
86+
87+
## Usage and API
88+
89+
`createActionListenerMiddleware` lets you add listeners by providing an action type and a callback, lets you specify whether your callback should run before or after the action is processed by the reducers, and gives you access to `dispatch` and `getState` for use in your logic. Callbacks can also unsubscribe.
90+
91+
Listeners can be defined statically by calling `listenerMiddleware.addListener()` during setup, or added and removed dynamically at runtime with special `dispatch(addListenerAction())` and `dispatch(removeListenerAction())` actions.
92+
93+
### `createActionListenerMiddleware`
94+
95+
Creates an instance of the middleware, which should then be added to the store via the `middleware` parameter.
96+
97+
### `listenerMiddleware.addListener(actionType, listener, options?) : Unsubscribe`
98+
99+
Statically adds a new listener callback to the middleware.
100+
101+
Parameters:
102+
103+
- `actionType: string | ActionCreator | Matcher`: Determines which action(s) will cause the `listener` callback to run. May be a plain action type string, a standard RTK-generated action creator with a `.type` field, or an RTK "matcher" function. The listener will be run if the current action's `action.type` string is an exact match, or if the matcher function returns true.
104+
- `listener: (action: Action, listenerApi: ListenerApi) => void`: the listener callback. Will receive the current action as its first argument. The second argument is a "listener API" object similar to the "thunk API" object in `createAsyncThunk`. It contains the usual `dispatch` and `getState` store methods, as well as two listener-specific methods: `unsubscribe` will remove the listener from the middleware, and `stopPropagation` will prevent any further listeners from handling this specific action.
105+
- `options: {when?: 'before' | 'after'}`: an options object. Currently only one options field is accepted - an enum indicating whether to run this listener 'before' the action is processed by the reducers, or 'after'. If not provided, the default is 'after'.
106+
107+
The return value is a standard `unsubscribe()` callback that will remove this listener.
108+
109+
### `addListenerAction`
110+
111+
A standard RTK action creator that tells the middleware to add a new listener at runtime. It accepts the same arguments as `listenerMiddleware.addListener()`.
112+
113+
Dispatching this action returns an `unsubscribe()` callback from `dispatch`.
114+
115+
### `removeListenerAction`
116+
117+
A standard RTK action creator that tells the middleware to remove a listener at runtime. It requires two arguments:
118+
119+
- `typeOrActionCreator: string | ActionCreator`: the same action type / action creator that was used to add the listener
120+
- `listener: ListenerCallback`: the same listener callback reference that was added originally
121+
122+
Note that matcher-based listeners currently cannot be removed with this approach - you must use the `unsubscribe()` callback that was returned when adding the listener.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@rtk-incubator/action-listener-middleware",
3+
"version": "0.0.1",
4+
"author": {
5+
"name": "Lenz Weber",
6+
"email": "[email protected]",
7+
"url": "https://phryneas.de/"
8+
},
9+
"license": "MIT",
10+
"type": "module",
11+
"source": "src/index.ts",
12+
"exports": "./dist/index.modern.js",
13+
"main": "./dist/index.cjs",
14+
"module": "./dist/index.module.js",
15+
"unpkg": "./dist/index.umd.js",
16+
"types": "./dist/index.d.ts",
17+
"scripts": {
18+
"build": "rimraf dist && microbundle",
19+
"dev": "microbundle watch",
20+
"prepublishOnly": "yarn build"
21+
},
22+
"peerDependencies": {
23+
"@reduxjs/toolkit": "^1.6.0"
24+
},
25+
"devDependencies": {
26+
"@reduxjs/toolkit": "^1.6.0",
27+
"microbundle": "^0.13.3",
28+
"rimraf": "^3.0.2",
29+
"typescript": "^4.3.4"
30+
},
31+
"publishConfig": {
32+
"access": "public"
33+
},
34+
"files": [
35+
"src",
36+
"dist"
37+
]
38+
}

0 commit comments

Comments
 (0)