Skip to content

Commit

Permalink
Add ability for slices to listen to other actions (#83)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
markerikson committed Jan 25, 2019
1 parent 5b38860 commit affc5d7
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 77 deletions.
19 changes: 7 additions & 12 deletions docs/api/configureStore.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ hide_title: true

# `configureStore`

A friendlier abstraction over the standard Redux `createStore` function.

A friendlier abstraction over the standard Redux `createStore` function.

## Parameters

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

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

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


### `middleware`

An optional array of Redux middleware functions.

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

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

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


### `devTools`

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

Defaults to true.

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.
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.

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


### `preloadedState`

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

### `enhancers`

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`.
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`.

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


## Usage

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

### Full Example
### Full Example

```js
import { configureStore, getDefaultMiddleware } from 'redux-starter-kit'
Expand Down
6 changes: 4 additions & 2 deletions docs/api/createReducer.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@ const counterReducer = createReducer(0, {
})
```

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

```js
const increment = createAction('increment')
const decrement = createAction('decrement')

const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement]: (state, action) => state - action.payload
[decrement.type]: (state, action) => state - action.payload
})
```

Expand Down
42 changes: 33 additions & 9 deletions docs/api/createSlice.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ A function that accepts an initial state, an object full of reducer functions, a
`createSlice` accepts a single configuration object parameter, with the following options:

```ts
function createSlice({
function configureStore({
// An object of "case reducers". Key names will be used to generate actions.
reducers: Object<string, ReducerFunction>
// The initial state for the reducer
initialState: any,
// An optional name, used in action types and selectors
slice?: string,
// An additional object of "case reducers". Keys should be other action types.
extraReducers?: Object<string, ReducerFunction>
})
```

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

### `extraReducers`

One of the key concepts of Redux is that each slice reducer "owns" its slice of state, and that many slice reducers
can independently respond to the same action type. `extraReducers` allows `createSlice` to respond to other action types
besides the types it has generated.

Like `reducers`, `extraReducers` should be an object containing Redux case reducer functions. However, the keys should
be other Redux string action type constants, and `createSlice` will _not_ auto-generate action types or action creators
for reducers included in this parameter.

As with `reducers`, these reducers will also be passed to `createReducer` and may "mutate" their state safely.

If two fields from `reducers` and `extraReducers` happen to end up with the same action type string,
the function from `reducers` will be used to handle that action type.

Action creators that were generated using [`createAction`](./createAction.md) may be used directly as the keys here, using
computed property syntax. (If you are using TypeScript, you may have to use `actionCreator.type` or `actionCreator.toString()`
to force the TS compiler to accept the computed property.)

## Return Value

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


## Examples

```js
Expand All @@ -107,11 +126,16 @@ const counter = createSlice({
const user = createSlice({
slice: 'user',
initialState: { name: '' },
initialState: { name: '', age: 20 },
reducers: {
setUserName: (state, action) => {
state.name = action.payload // mutate the state all you want with immer
}
},
extraReducers: {
[counter.actions.increment]: (state, action) => {
state.age += 1
}
}
})
Expand All @@ -123,18 +147,18 @@ const reducer = combineReducers({
const store = createStore(reducer)
store.dispatch(counter.actions.increment())
// -> { counter: 1, user: {} }
// -> { counter: 1, user: {name : '', age: 20} }
store.dispatch(counter.actions.increment())
// -> { counter: 2, user: {} }
// -> { counter: 2, user: {name: '', age: 21} }
store.dispatch(counter.actions.multiply(3))
// -> { counter: 6, user: {} }
// -> { counter: 6, user: {name: '', age: 22} }
console.log(`${counter.actions.decrement}`)
// -> counter/decrement
// -> "counter/decrement"
store.dispatch(user.actions.setUserName('eric'))
// -> { counter: 6, user: { name: 'eric' } }
// -> { counter: 6, user: { name: 'eric', age: 22} }
const state = store.getState()
console.log(user.selectors.getUser(state))
// -> { name: 'eric' }
// -> { name: 'eric', age: 22 }
console.log(counter.selectors.getCounter(state))
// -> 6
```
35 changes: 17 additions & 18 deletions docs/api/getDefaultMiddleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ By default, [`configureStore`](./configureStore.md) adds some middleware to the

```js
const store = configureStore({
reducer : rootReducer,
reducer: rootReducer
})

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

```js
const store = configureStore({
reducer : rootReducer,
middleware : [thunk, logger],
reducer: rootReducer,
middleware: [thunk, logger]
})

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

```js
const store = configureStore({
reducer : rootReducer,
middleware: [...getDefaultMiddleware(), logger]
reducer: rootReducer,
middleware: [...getDefaultMiddleware(), logger]
})

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


## Included Default Middleware

### Development

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

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

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

Currently, the return value is:

```js
[immutableStateInvariant, thunk, serializableStateInvariant]
;[immutableStateInvariant, thunk, serializableStateInvariant]
```

### Production

Currently, the return value is:

```js
[thunk]
;[thunk]
```
20 changes: 10 additions & 10 deletions docs/api/otherExports.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,32 @@ hide_title: true

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


## Internal Exports


### `createSerializableStateInvariantMiddleware`

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

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

Example:

```js
import {configureStore, createSerializableStateInvariantMiddleware} from "redux-starter-kit";
import {
configureStore,
createSerializableStateInvariantMiddleware
} from 'redux-starter-kit'

const serializableMiddleware = createSerializableStateInvariantMiddleware({
isSerializable: () => true // all values will be accepted
});
isSerializable: () => true // all values will be accepted
})

const store = configureStore({
reducer,
middleware : [serializableMiddleware],
});
reducer,
middleware: [serializableMiddleware]
})
```

### `isPlain`
Expand All @@ -56,7 +57,6 @@ function isPlain(val) {
}
```


## Exports from Other Libraries

### `createNextState`
Expand Down
7 changes: 5 additions & 2 deletions src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface PayloadAction<P = any, T extends string = string>
export interface PayloadActionCreator<P = any, T extends string = string> {
(): Action<T>
(payload: P): PayloadAction<P, T>
type: T
}

/**
Expand All @@ -31,14 +32,16 @@ export interface PayloadActionCreator<P = any, T extends string = string> {
*/
export function createAction<P = any, T extends string = string>(
type: T
): PayloadActionCreator<P> {
): PayloadActionCreator<P, T> {
function actionCreator(): Action<T>
function actionCreator(payload: P): PayloadAction<P, T>
function actionCreator(payload?: P): Action<T> | PayloadAction<P, T> {
return { type, payload }
}

actionCreator.toString = () => `${type}`
actionCreator.toString = (): T => `${type}` as T

actionCreator.type = type

return actionCreator
}
Expand Down
22 changes: 22 additions & 0 deletions src/createSlice.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSlice } from './createSlice'
import { createAction } from './createAction'

describe('createSlice', () => {
describe('when slice is empty', () => {
Expand Down Expand Up @@ -105,4 +106,25 @@ describe('createSlice', () => {
})
})
})

describe('when passing extra reducers', () => {
const addMore = createAction('ADD_MORE')

const { reducer } = createSlice({
reducers: {
increment: state => state + 1,
multiply: (state, action) => state * action.payload
},
extraReducers: {
[addMore.type]: (state, action) => state + action.payload.amount
},
initialState: 0
})

it('should call extra reducers when their actions are dispatched', () => {
const result = reducer(10, addMore({ amount: 5 }))

expect(result).toBe(15)
})
})
})
Loading

0 comments on commit affc5d7

Please sign in to comment.