Skip to content
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

Remove toString override from action creators, in favour of explicit .type field. #3425

Merged
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
17 changes: 8 additions & 9 deletions docs/api/createAction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const action = increment(3)
// { type: 'counter/increment', payload: 3 }
```

The `createAction` helper combines these two declarations into one. It takes an action type and returns an action creator for that type. The action creator can be called either without arguments or with a `payload` to be attached to the action. Also, the action creator overrides [toString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString) so that the action type becomes its string representation.
The `createAction` helper combines these two declarations into one. It takes an action type and returns an action creator for that type. The action creator can be called either without arguments or with a `payload` to be attached to the action.

```ts
import { createAction } from '@reduxjs/toolkit'
Expand All @@ -44,10 +44,7 @@ let action = increment()
action = increment(3)
// returns { type: 'counter/increment', payload: 3 }

console.log(increment.toString())
// 'counter/increment'

console.log(`The action type is: ${increment}`)
console.log(`The action type is: ${increment.type}`)
// 'The action type is: counter/increment'
```

Expand Down Expand Up @@ -89,7 +86,7 @@ If provided, all arguments from the action creator will be passed to the prepare

## Usage with createReducer()

Because of their `toString()` override, action creators returned by `createAction()` can be used directly as keys for the case reducers passed to [createReducer()](createReducer.mdx).
Action creators can be passed directly to `addCase` in a [createReducer()](createReducer.mdx) build callback.

```ts
import { createAction, createReducer } from '@reduxjs/toolkit'
Expand All @@ -103,21 +100,23 @@ const counterReducer = createReducer(0, (builder) => {
})
```

<!-- TODO: how do we handle this? -->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I dunno, what "this" are we referring to here? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have anything remaining that actually depends on the type being a string - but I'm assuming we still want to advise that they should be strings?


## Non-String Action Types

In principle, Redux lets you use any kind of value as an action type. Instead of strings, you could theoretically use numbers, [symbols](https://developer.mozilla.org/en-US/docs/Glossary/Symbol), or anything else ([although it's recommended that the value should at least be serializable](https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)).

However, Redux Toolkit rests on the assumption that you use string action types. Specifically, some of its features rely on the fact that with strings, the `toString()` method of an `createAction()` action creator returns the matching action type. This is not the case for non-string action types because `toString()` will return the string-converted type value rather than the type itself.
However, Redux Toolkit rests on the assumption that you use string action types.

```js
const INCREMENT = Symbol('increment')
const increment = createAction(INCREMENT)

increment.toString()
increment.type.toString()
// returns the string 'Symbol(increment)',
// not the INCREMENT symbol itself

increment.toString() === INCREMENT
increment.type.toString() === INCREMENT
// false
```

Expand Down
2 changes: 1 addition & 1 deletion docs/introduction/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Redux Toolkit includes these APIs:

- [`configureStore()`](../api/configureStore.mdx): wraps `createStore` to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes `redux-thunk` by default, and enables use of the Redux DevTools Extension.
- [`createReducer()`](../api/createReducer.mdx): that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the [`immer` library](https://github.com/immerjs/immer) to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
- [`createAction()`](../api/createAction.mdx): generates an action creator function for the given action type string. The function itself has `toString()` defined, so that it can be used in place of the type constant.
- [`createAction()`](../api/createAction.mdx): generates an action creator function for the given action type string.
- [`createSlice()`](../api/createSlice.mdx): accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
- [`createAsyncThunk`](../api/createAsyncThunk.mdx): accepts an action type string and a function that returns a promise, and generates a thunk that dispatches `pending/fulfilled/rejected` action types based on that promise
- [`createEntityAdapter`](../api/createEntityAdapter.mdx): generates a set of reusable reducers and selectors to manage normalized data in the store
Expand Down
36 changes: 11 additions & 25 deletions docs/usage/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,24 +279,16 @@ addTodo({ text: 'Buy milk' })

### Using Action Creators as Action Types

Redux reducers need to look for specific action types to determine how they should update their state. Normally, this is done by defining action type strings and action creator functions separately. Redux Toolkit `createAction` function uses a couple tricks to make this easier.

First, `createAction` overrides the `toString()` method on the action creators it generates. **This means that the action creator itself can be used as the "action type" reference in some places**, such as the keys provided to `builder.addCase` or the `createReducer` object notation.

Second, the action type is also defined as a `type` field on the action creator.
Redux reducers need to look for specific action types to determine how they should update their state. Normally, this is done by defining action type strings and action creator functions separately. Redux Toolkit `createAction` function make this easier, by defining the action type as a `type` field on the action creator.

```js
const actionCreator = createAction('SOME_ACTION_TYPE')

console.log(actionCreator.toString())
// "SOME_ACTION_TYPE"

console.log(actionCreator.type)
// "SOME_ACTION_TYPE"

const reducer = createReducer({}, (builder) => {
// actionCreator.toString() will automatically be called here
// also, if you use TypeScript, the action type will be correctly inferred
// if you use TypeScript, the action type will be correctly inferred
builder.addCase(actionCreator, (state, action) => {})

// Or, you can reference the .type field:
Expand All @@ -307,7 +299,7 @@ const reducer = createReducer({}, (builder) => {

This means you don't have to write or use a separate action type variable, or repeat the name and value of an action type like `const SOME_ACTION_TYPE = "SOME_ACTION_TYPE"`.

Unfortunately, the implicit conversion to a string doesn't happen for switch statements. If you want to use one of these action creators in a switch statement, you need to call `actionCreator.toString()` yourself:
If you want to use one of these action creators in a switch statement, you need to call `actionCreator.type` yourself:

```js
const actionCreator = createAction('SOME_ACTION_TYPE')
Expand All @@ -319,19 +311,13 @@ const reducer = (state = {}, action) => {
break
}
// CORRECT: this will work as expected
case actionCreator.toString(): {
break
}
// CORRECT: this will also work right
case actionCreator.type: {
break
}
}
}
```

If you are using Redux Toolkit with TypeScript, note that the TypeScript compiler may not accept the implicit `toString()` conversion when the action creator is used as an object key. In that case, you may need to either manually cast it to a string (`actionCreator as string`), or use the `.type` field as the key.

## Creating Slices of State

Redux state is typically organized into "slices", defined by the reducers that are passed to `combineReducers`:
Expand Down Expand Up @@ -619,17 +605,17 @@ A typical implementation might look like:

```js
const getRepoDetailsStarted = () => ({
type: "repoDetails/fetchStarted"
type: 'repoDetails/fetchStarted',
})
const getRepoDetailsSuccess = (repoDetails) => ({
type: "repoDetails/fetchSucceeded",
payload: repoDetails
type: 'repoDetails/fetchSucceeded',
payload: repoDetails,
})
const getRepoDetailsFailed = (error) => ({
type: "repoDetails/fetchFailed",
error
type: 'repoDetails/fetchFailed',
error,
})
const fetchIssuesCount = (org, repo) => async dispatch => {
const fetchIssuesCount = (org, repo) => async (dispatch) => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
Expand Down Expand Up @@ -1118,11 +1104,11 @@ It is also strongly recommended to blacklist any api(s) that you have configured

```ts
const persistConfig = {
key: "root",
key: 'root',
version: 1,
storage,
blacklist: [pokemonApi.reducerPath],
};
}
```

See [Redux Toolkit #121: How to use this with Redux-Persist?](https://github.com/reduxjs/redux-toolkit/issues/121) and [Redux-Persist #988: non-serializable value error](https://github.com/rt2zz/redux-persist/issues/988#issuecomment-552242978) for further discussion.
Expand Down
26 changes: 2 additions & 24 deletions packages/toolkit/src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,7 @@ export type PayloadActionCreator<
/**
* 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.
* in the action object as a field called payload.
*
* @param type The action type to use for created actions.
* @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
Expand All @@ -241,9 +239,7 @@ export function createAction<P = void, T extends string = string>(
/**
* 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.
* in the action object as a field called payload.
*
* @param type The action type to use for created actions.
* @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
Expand Down Expand Up @@ -277,8 +273,6 @@ export function createAction(type: string, prepareAction?: Function): any {
return { type, payload: args[0] }
}

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

actionCreator.type = type

actionCreator.match = (action: Action<unknown>): action is PayloadAction =>
Expand Down Expand Up @@ -328,22 +322,6 @@ function isValidKey(key: string) {
return ['type', 'payload', 'error', 'meta'].indexOf(key) > -1
}

/**
* Returns the action type of the actions created by the passed
* `createAction()`-generated action creator (arbitrary action creators
* are not supported).
*
* @param action The action creator whose action type to get.
* @returns The action type used by the action creator.
*
* @public
*/
export function getType<T extends string>(
actionCreator: PayloadActionCreator<any, T>
): T {
return `${actionCreator}` as T
}

// helper types for more readable typings

type IfPrepareActionMethodProvided<
Expand Down
1 change: 0 additions & 1 deletion packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export type { DevToolsEnhancerOptions } from './devtoolsExtension'
export {
// js
createAction,
getType,
isAction,
isActionCreator,
isFSA as isFluxStandardAction,
Expand Down
38 changes: 22 additions & 16 deletions packages/toolkit/src/tests/createAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createAction, getType, isAction } from '@reduxjs/toolkit'
import { createAction, isAction, isActionCreator } from '@reduxjs/toolkit'

describe('createAction', () => {
it('should create an action', () => {
Expand All @@ -9,13 +9,6 @@ describe('createAction', () => {
})
})

describe('when stringifying action', () => {
it('should return the action type', () => {
const actionCreator = createAction('A_TYPE')
expect(`${actionCreator}`).toEqual('A_TYPE')
})
})

describe('when passing a prepareAction method only returning a payload', () => {
it('should use the payload returned from the prepareAction method', () => {
const actionCreator = createAction('A_TYPE', (a: number) => ({
Expand Down Expand Up @@ -122,12 +115,13 @@ describe('createAction', () => {
})
})

const actionCreator = createAction('anAction')

class Action {
type = 'totally an action'
}
describe('isAction', () => {
it('should only return true for plain objects with a type property', () => {
const actionCreator = createAction('anAction')
class Action {
type = 'totally an action'
}
const testCases: [action: unknown, expected: boolean][] = [
[{ type: 'an action' }, true],
[{ type: 'more props', extra: true }, true],
Expand All @@ -143,9 +137,21 @@ describe('isAction', () => {
})
})

describe('getType', () => {
it('should return the action type', () => {
const actionCreator = createAction('A_TYPE')
expect(getType(actionCreator)).toEqual('A_TYPE')
describe('isActionCreator', () => {
it('should only return true for action creators', () => {
expect(isActionCreator(actionCreator)).toBe(true)
const notActionCreators = [
{ type: 'an action' },
{ type: 'more props', extra: true },
actionCreator(),
Promise.resolve({ type: 'an action' }),
new Action(),
false,
'a string',
false,
]
for (const notActionCreator of notActionCreators) {
expect(isActionCreator(notActionCreator)).toBe(false)
}
})
})