Skip to content

Commit affc5d7

Browse files
authored
Add ability for slices to listen to other actions (#83)
* Add `type` field to action creators * Update createSlice to handle other actions * Fill out createSlice docs * Formatting * Hacky attempt to fix createAction type tests * Fix example typo * Alternate: Add ability for slices to listen to other actions (#86) Implemented my proposed changes from #83 (comment) @denisw @markerikson your inputs are appreciated *Note: this PR is for the `feature/other-slice-action` branch not `master`* * ~~Removed `getType()` utility~~ * ~~`slice` is no longer attached to `actionsMap` and `reducerMap`~~ * ~~Removed `reducerMap` and directly forward `reducers` to `createReducer`~~ - [x] `createAction()` action creators `type` property returns a `string literal` for better type saftey - [x] Fixed tests - [x] Added tests * Play with type tests a bit
1 parent 5b38860 commit affc5d7

File tree

10 files changed

+156
-77
lines changed

10 files changed

+156
-77
lines changed

docs/api/configureStore.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ hide_title: true
77

88
# `configureStore`
99

10-
A friendlier abstraction over the standard Redux `createStore` function.
11-
10+
A friendlier abstraction over the standard Redux `createStore` function.
1211

1312
## Parameters
1413

@@ -34,48 +33,44 @@ function configureStore({
3433

3534
If this is a single function, it will be directly used as the root reducer for the store.
3635

37-
If it is an object of slice reducers, like `{users : usersReducer, posts : postsReducer}`,
36+
If it is an object of slice reducers, like `{users : usersReducer, posts : postsReducer}`,
3837
`configureStore` will automatically create the root reducer by passing this object to the
3938
[Redux `combineReducers` utility](https://redux.js.org/api/combinereducers).
4039

41-
4240
### `middleware`
4341

4442
An optional array of Redux middleware functions.
4543

4644
If this option is provided, it should contain all the middleware functions you
47-
want added to the store. `configureStore` will automatically pass those to `applyMiddleware`.
45+
want added to the store. `configureStore` will automatically pass those to `applyMiddleware`.
4846

4947
If not provided, `configureStore` will call `getDefaultMiddleware` and use the
5048
array of middleware functions it returns.
5149

5250
For more details on how the `middleware` parameter works and the list of middleware that are added by default, see the
5351
[`getDefaultMiddleware` docs page](./getDefaultMiddleware.md).
5452

55-
5653
### `devTools`
5754

58-
A boolean indicating whether `configureStore` should automatically enable support for [the Redux DevTools browser extension](https://github.com/zalmoxisus/redux-devtools-extension).
55+
A boolean indicating whether `configureStore` should automatically enable support for [the Redux DevTools browser extension](https://github.com/zalmoxisus/redux-devtools-extension).
5956

6057
Defaults to true.
6158

62-
The Redux DevTools Extension recently added [support for showing action stack traces](https://github.com/zalmoxisus/redux-devtools-extension/blob/d4ef75691ad294646f74bca38b973b19850a37cf/docs/Features/Trace.md) that show exactly where each action was dispatched. Capturing the traces can add a bit of overhead, so the DevTools Extension allows users to configure whether action stack traces are captured.
59+
The Redux DevTools Extension recently added [support for showing action stack traces](https://github.com/zalmoxisus/redux-devtools-extension/blob/d4ef75691ad294646f74bca38b973b19850a37cf/docs/Features/Trace.md) that show exactly where each action was dispatched. Capturing the traces can add a bit of overhead, so the DevTools Extension allows users to configure whether action stack traces are captured.
6360

6461
If this parameter is true, then `configureStore` will enable capturing action stack traces in development mode only.
6562

66-
6763
### `preloadedState`
6864

6965
An optional initial state value to be passed to the Redux `createStore` function.
7066

7167
### `enhancers`
7268

73-
An optional array of Redux store enhancers. If included, these will be passed to [the Redux `compose` function](https://redux.js.org/api/compose), and the combined enhancer will be passed to `createStore`.
69+
An optional array of Redux store enhancers. If included, these will be passed to [the Redux `compose` function](https://redux.js.org/api/compose), and the combined enhancer will be passed to `createStore`.
7470

7571
This should _not_ include `applyMiddleware()` or
7672
the Redux DevTools Extension `composeWithDevTools`, as those are already handled by `configureStore`.
7773

78-
7974
## Usage
8075

8176
### Basic Example
@@ -89,7 +84,7 @@ const store = configureStore({ reducer: rootReducer })
8984
// The store now has redux-thunk added and the Redux DevTools Extension is turned on
9085
```
9186

92-
### Full Example
87+
### Full Example
9388

9489
```js
9590
import { configureStore, getDefaultMiddleware } from 'redux-starter-kit'

docs/api/createReducer.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,17 @@ const counterReducer = createReducer(0, {
3535
})
3636
```
3737

38-
If you created action creators using `createAction()`, you can use those directly as keys for the case reducers.
38+
Action creators that were generated using [`createAction`](./createAction.md) may be used directly as the keys here, using
39+
computed property syntax. (If you are using TypeScript, you may have to use `actionCreator.type` or `actionCreator.toString()`
40+
to force the TS compiler to accept the computed property.)
3941

4042
```js
4143
const increment = createAction('increment')
4244
const decrement = createAction('decrement')
4345

4446
const counterReducer = createReducer(0, {
4547
[increment]: (state, action) => state + action.payload,
46-
[decrement]: (state, action) => state - action.payload
48+
[decrement.type]: (state, action) => state - action.payload
4749
})
4850
```
4951

docs/api/createSlice.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ A function that accepts an initial state, an object full of reducer functions, a
1414
`createSlice` accepts a single configuration object parameter, with the following options:
1515

1616
```ts
17-
function createSlice({
17+
function configureStore({
1818
// An object of "case reducers". Key names will be used to generate actions.
1919
reducers: Object<string, ReducerFunction>
2020
// The initial state for the reducer
2121
initialState: any,
2222
// An optional name, used in action types and selectors
2323
slice?: string,
24+
// An additional object of "case reducers". Keys should be other action types.
25+
extraReducers?: Object<string, ReducerFunction>
2426
})
2527
```
2628

@@ -54,6 +56,24 @@ will be generated. This selector assume the slice data exists in an object, with
5456
return the value at that key name. If not provided, a selector named `getState` will be generated that just returns
5557
its argument.
5658

59+
### `extraReducers`
60+
61+
One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers
62+
can independently respond to the same action type. `extraReducers` allows `createSlice` to respond to other action types
63+
besides the types it has generated.
64+
65+
Like `reducers`, `extraReducers` should be an object containing Redux case reducer functions. However, the keys should
66+
be other Redux string action type constants, and `createSlice` will _not_ auto-generate action types or action creators
67+
for reducers included in this parameter.
68+
69+
As with `reducers`, these reducers will also be passed to `createReducer` and may "mutate" their state safely.
70+
71+
If two fields from `reducers` and `extraReducers` happen to end up with the same action type string,
72+
the function from `reducers` will be used to handle that action type.
73+
74+
Action creators that were generated using [`createAction`](./createAction.md) may be used directly as the keys here, using
75+
computed property syntax. (If you are using TypeScript, you may have to use `actionCreator.type` or `actionCreator.toString()`
76+
to force the TS compiler to accept the computed property.)
5777

5878
## Return Value
5979

@@ -88,7 +108,6 @@ for references in a larger codebase.
88108
> separate files, and each file tries to import the other so it can listen to other actions, unexpected
89109
> behavior may occur.
90110

91-
92111
## Examples
93112

94113
```js
@@ -107,11 +126,16 @@ const counter = createSlice({
107126
108127
const user = createSlice({
109128
slice: 'user',
110-
initialState: { name: '' },
129+
initialState: { name: '', age: 20 },
111130
reducers: {
112131
setUserName: (state, action) => {
113132
state.name = action.payload // mutate the state all you want with immer
114133
}
134+
},
135+
extraReducers: {
136+
[counter.actions.increment]: (state, action) => {
137+
state.age += 1
138+
}
115139
}
116140
})
117141
@@ -123,18 +147,18 @@ const reducer = combineReducers({
123147
const store = createStore(reducer)
124148
125149
store.dispatch(counter.actions.increment())
126-
// -> { counter: 1, user: {} }
150+
// -> { counter: 1, user: {name : '', age: 20} }
127151
store.dispatch(counter.actions.increment())
128-
// -> { counter: 2, user: {} }
152+
// -> { counter: 2, user: {name: '', age: 21} }
129153
store.dispatch(counter.actions.multiply(3))
130-
// -> { counter: 6, user: {} }
154+
// -> { counter: 6, user: {name: '', age: 22} }
131155
console.log(`${counter.actions.decrement}`)
132-
// -> counter/decrement
156+
// -> "counter/decrement"
133157
store.dispatch(user.actions.setUserName('eric'))
134-
// -> { counter: 6, user: { name: 'eric' } }
158+
// -> { counter: 6, user: { name: 'eric', age: 22} }
135159
const state = store.getState()
136160
console.log(user.selectors.getUser(state))
137-
// -> { name: 'eric' }
161+
// -> { name: 'eric', age: 22 }
138162
console.log(counter.selectors.getCounter(state))
139163
// -> 6
140164
```

docs/api/getDefaultMiddleware.md

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ By default, [`configureStore`](./configureStore.md) adds some middleware to the
1515

1616
```js
1717
const store = configureStore({
18-
reducer : rootReducer,
18+
reducer: rootReducer
1919
})
2020

2121
// Store has one or more middleware added, because the middleware list was not customized
@@ -25,8 +25,8 @@ If you want to customize the list of middleware, you can supply an array of midd
2525

2626
```js
2727
const store = configureStore({
28-
reducer : rootReducer,
29-
middleware : [thunk, logger],
28+
reducer: rootReducer,
29+
middleware: [thunk, logger]
3030
})
3131

3232
// Store specifically has the thunk and logger middleware applied
@@ -40,44 +40,43 @@ middleware added as well:
4040

4141
```js
4242
const store = configureStore({
43-
reducer : rootReducer,
44-
middleware: [...getDefaultMiddleware(), logger]
43+
reducer: rootReducer,
44+
middleware: [...getDefaultMiddleware(), logger]
4545
})
4646

4747
// Store has all of the default middleware added, _plus_ the logger middleware
4848
```
4949

50-
5150
## Included Default Middleware
5251

5352
### Development
5453

55-
One of the goals of `redux-starter-kit` is to provide opinionated defaults and prevent common mistakes. As part of that,
56-
`getDefaultMiddleware` includes some middleware that are added **in development builds of your app only** to
54+
One of the goals of `redux-starter-kit` is to provide opinionated defaults and prevent common mistakes. As part of that,
55+
`getDefaultMiddleware` includes some middleware that are added **in development builds of your app only** to
5756
provide runtime checks for two common issues:
5857

59-
- [`redux-immutable-state-invariant`](https://github.com/leoasis/redux-immutable-state-invariant): deeply compares
60-
state values for mutations. It can detect mutations in reducers during a dispatch, and also mutations that occur between
61-
dispatches (such as in a component or a selector). When a mutation is detect, it will throw an error and indicate the key
62-
path for where the mutated value was detected in the state tree.
63-
- `serializable-state-invariant-middleware`: a custom middleware created specifically for use in `redux-starter-kit`. Similar in
64-
concept to `redux-immutable-state-invariant`, but deeply checks your state tree and your actions for non-serializable values
65-
such as functions, Promises, Symbols, and other non-plain-JS-data values. When a non-serializable value is detected, a
66-
console error will be printed with the key path for where the non-serializable value was detected.
58+
- [`redux-immutable-state-invariant`](https://github.com/leoasis/redux-immutable-state-invariant): deeply compares
59+
state values for mutations. It can detect mutations in reducers during a dispatch, and also mutations that occur between
60+
dispatches (such as in a component or a selector). When a mutation is detect, it will throw an error and indicate the key
61+
path for where the mutated value was detected in the state tree.
62+
- `serializable-state-invariant-middleware`: a custom middleware created specifically for use in `redux-starter-kit`. Similar in
63+
concept to `redux-immutable-state-invariant`, but deeply checks your state tree and your actions for non-serializable values
64+
such as functions, Promises, Symbols, and other non-plain-JS-data values. When a non-serializable value is detected, a
65+
console error will be printed with the key path for where the non-serializable value was detected.
6766

6867
In addition to these development tool middleware, it also adds [`redux-thunk`](https://github.com/reduxjs/redux-thunk)
6968
by default, since thunks are the basic recommended side effects middleware for Redux.
7069

7170
Currently, the return value is:
7271

7372
```js
74-
[immutableStateInvariant, thunk, serializableStateInvariant]
73+
;[immutableStateInvariant, thunk, serializableStateInvariant]
7574
```
7675

7776
### Production
7877

7978
Currently, the return value is:
8079

8180
```js
82-
[thunk]
81+
;[thunk]
8382
```

docs/api/otherExports.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,32 @@ hide_title: true
99

1010
`redux-starter-kit` exports some of its internal utilities, and re-exports additional functions from other dependencies as well.
1111

12-
1312
## Internal Exports
1413

15-
1614
### `createSerializableStateInvariantMiddleware`
1715

1816
Creates an instance of the `serializable-state-invariant` middleware described in [`getDefaultMiddleware`](./getDefaultMiddleware.md).
1917

2018
Accepts an options object with an `isSerializable` parameter, which will be used
21-
to determine if a value is considered serializable or not. If not provided, this
19+
to determine if a value is considered serializable or not. If not provided, this
2220
defaults to `isPlain`.
2321

2422
Example:
2523

2624
```js
27-
import {configureStore, createSerializableStateInvariantMiddleware} from "redux-starter-kit";
25+
import {
26+
configureStore,
27+
createSerializableStateInvariantMiddleware
28+
} from 'redux-starter-kit'
2829

2930
const serializableMiddleware = createSerializableStateInvariantMiddleware({
30-
isSerializable: () => true // all values will be accepted
31-
});
31+
isSerializable: () => true // all values will be accepted
32+
})
3233

3334
const store = configureStore({
34-
reducer,
35-
middleware : [serializableMiddleware],
36-
});
35+
reducer,
36+
middleware: [serializableMiddleware]
37+
})
3738
```
3839

3940
### `isPlain`
@@ -56,7 +57,6 @@ function isPlain(val) {
5657
}
5758
```
5859

59-
6060
## Exports from Other Libraries
6161

6262
### `createNextState`

src/createAction.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface PayloadAction<P = any, T extends string = string>
1818
export interface PayloadActionCreator<P = any, T extends string = string> {
1919
(): Action<T>
2020
(payload: P): PayloadAction<P, T>
21+
type: T
2122
}
2223

2324
/**
@@ -31,14 +32,16 @@ export interface PayloadActionCreator<P = any, T extends string = string> {
3132
*/
3233
export function createAction<P = any, T extends string = string>(
3334
type: T
34-
): PayloadActionCreator<P> {
35+
): PayloadActionCreator<P, T> {
3536
function actionCreator(): Action<T>
3637
function actionCreator(payload: P): PayloadAction<P, T>
3738
function actionCreator(payload?: P): Action<T> | PayloadAction<P, T> {
3839
return { type, payload }
3940
}
4041

41-
actionCreator.toString = () => `${type}`
42+
actionCreator.toString = (): T => `${type}` as T
43+
44+
actionCreator.type = type
4245

4346
return actionCreator
4447
}

src/createSlice.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createSlice } from './createSlice'
2+
import { createAction } from './createAction'
23

34
describe('createSlice', () => {
45
describe('when slice is empty', () => {
@@ -105,4 +106,25 @@ describe('createSlice', () => {
105106
})
106107
})
107108
})
109+
110+
describe('when passing extra reducers', () => {
111+
const addMore = createAction('ADD_MORE')
112+
113+
const { reducer } = createSlice({
114+
reducers: {
115+
increment: state => state + 1,
116+
multiply: (state, action) => state * action.payload
117+
},
118+
extraReducers: {
119+
[addMore.type]: (state, action) => state + action.payload.amount
120+
},
121+
initialState: 0
122+
})
123+
124+
it('should call extra reducers when their actions are dispatched', () => {
125+
const result = reducer(10, addMore({ amount: 5 }))
126+
127+
expect(result).toBe(15)
128+
})
129+
})
108130
})

0 commit comments

Comments
 (0)