Skip to content

Add ability for slices to listen to other actions #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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