Skip to content

Commit 471e8ea

Browse files
authored
Assorted docs tweaks (#932)
- Updated Immer docs URLs - Made TS examples consistent with other sites - Added `no-param-reassign` section in "Immer Reducers" page
1 parent f41dc23 commit 471e8ea

File tree

6 files changed

+61
-23
lines changed

6 files changed

+61
-23
lines changed

docs/api/createReducer.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ const todosReducer = createReducer([] as Todo[], builder => {
219219
})
220220
```
221221

222-
Writing "mutating" reducers simplifies the code. It's shorter, there's less indirection, and it eliminates common mistakes made while spreading nested state. However, the use of Immer does add some "magic", and Immer has its own nuances in behavior. You should read through [pitfalls mentioned in the immer docs](https://immerjs.github.io/immer/docs/pitfalls) . Most importantly, **you need to ensure that you either mutate the `state` argument or return a new state, _but not both_**. For example, the following reducer would throw an exception if a `toggleTodo` action is passed:
222+
Writing "mutating" reducers simplifies the code. It's shorter, there's less indirection, and it eliminates common mistakes made while spreading nested state. However, the use of Immer does add some "magic", and Immer has its own nuances in behavior. You should read through [pitfalls mentioned in the immer docs](https://immerjs.github.io/immer/pitfalls) . Most importantly, **you need to ensure that you either mutate the `state` argument or return a new state, _but not both_**. For example, the following reducer would throw an exception if a `toggleTodo` action is passed:
223223

224224
```ts
225225
import { createAction, createReducer } from '@reduxjs/toolkit'
@@ -289,7 +289,7 @@ console.log(reducer(0, { type: 'increment' }))
289289

290290
It's very common for a developer to call `console.log(state)` during the development process. However, browsers display Proxies in a format that is hard to read, which can make console logging of Immer-based state difficult.
291291

292-
When using either `createSlice` or `createReducer`, you may use the [`current`](./otherExports.mdx#current) utility that we re-export from the [`immer` library](https://immerjs.github.io/immer/docs/current). This utility creates a separate plain copy of the current Immer `Draft` state value, which can then be logged for viewing as normal.
292+
When using either `createSlice` or `createReducer`, you may use the [`current`](./otherExports.mdx#current) utility that we re-export from the [`immer` library](https://immerjs.github.io/immer/current). This utility creates a separate plain copy of the current Immer `Draft` state value, which can then be logged for viewing as normal.
293293

294294
```ts
295295
import { createSlice, current } from '@reduxjs/toolkit'

docs/api/otherExports.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ console.log(nanoid())
2424

2525
### `createNextState`
2626

27-
The default immutable update function from the [`immer` library](https://immerjs.github.io/immer/), re-exported here as `createNextState` (also commonly referred to as [`produce`](https://immerjs.github.io/immer/docs/produce))
27+
The default immutable update function from the [`immer` library](https://immerjs.github.io/immer/), re-exported here as `createNextState` (also commonly referred to as [`produce`](https://immerjs.github.io/immer/produce))
2828

2929
### `current`
3030

31-
[The `current` function](https://immerjs.github.io/immer/docs/current) from the [`immer` library](https://immerjs.github.io/immer/), which takes a snapshot of the current state of a draft and finalizes it (but without freezing). Current is a great utility to print the current state during debugging, and the output of `current` can also be safely leaked outside the producer.
31+
[The `current` function](https://immerjs.github.io/immer/current) from the [`immer` library](https://immerjs.github.io/immer/), which takes a snapshot of the current state of a draft and finalizes it (but without freezing). Current is a great utility to print the current state during debugging, and the output of `current` can also be safely leaked outside the producer.
3232

3333
### `original`
3434

35-
[The `original` function](https://immerjs.github.io/immer/docs/original) from the [`immer` library](https://immerjs.github.io/immer/), which returns the original object. This is particularly useful for referential equality check in reducers.
35+
[The `original` function](https://immerjs.github.io/immer/original) from the [`immer` library](https://immerjs.github.io/immer/), which returns the original object. This is particularly useful for referential equality check in reducers.
3636

3737
```ts
3838
import { createReducer, createAction, current } from '@reduxjs/toolkit'

docs/tutorials/quick-start.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Add a new file named `src/features/counter/counterSlice.js`. In that file, impor
8686

8787
Creating a slice requires a string name to identify the slice, an initial state value, and one or more reducer functions to define how the state can be updated. Once a slice is created, we can export the generated Redux action creators and the reducer function for the whole slice.
8888

89-
Redux requires that [we write all state updates immutably, by making copies of data and updating the copies](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow#immutability). However, Redux Toolkit's `createSlice` and `createReducer` APIs use [Immer](https://immerjs.github.io/immer/docs/introduction) inside to allow us to [write "mutating" update logic that becomes correct immutable updates](https://redux.js.org/tutorials/fundamentals/part-8-modern-redux#immutable-updates-with-immer).
89+
Redux requires that [we write all state updates immutably, by making copies of data and updating the copies](https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow#immutability). However, Redux Toolkit's `createSlice` and `createReducer` APIs use [Immer](https://immerjs.github.io/immer/introduction) inside to allow us to [write "mutating" update logic that becomes correct immutable updates](https://redux.js.org/tutorials/fundamentals/part-8-modern-redux#immutable-updates-with-immer).
9090

9191
```js title="features/counter/counterSlice.js"
9292
import { createSlice } from '@reduxjs/toolkit'

docs/tutorials/typescript.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,32 @@ import { configureStore } from '@reduxjs/toolkit'
4343

4444
const store = configureStore({
4545
reducer: {
46-
one: oneSlice.reducer,
47-
two: twoSlice.reducer
46+
posts: postsReducer,
47+
comments: commentsReducer,
48+
users: usersReducer
4849
}
4950
})
5051

5152
// highlight-start
5253
// Infer the `RootState` and `AppDispatch` types from the store itself
5354
export type RootState = ReturnType<typeof store.getState>
55+
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
5456
export type AppDispatch = typeof store.dispatch
5557
// highlight-end
5658
```
5759
5860
### Define Typed Hooks
5961
60-
While it's possible to import the `RootState` and `AppDispatch` types into each component, it's better to create typed versions of the `useDispatch` and `useSelector` hooks for usage in your application. Since these are actual variables, not types, it's important to define them in a separate file such as `app/hooks.ts`, not the store setup file. This allows you to import them into any component file that needs to use the hooks, and avoids potential circular import dependency issues.
62+
While it's possible to import the `RootState` and `AppDispatch` types into each component, it's **better to create typed versions of the `useDispatch` and `useSelector` hooks for usage in your application**. . This is important for a couple reasons:
63+
64+
- For `useSelector`, it saves you the need to type `(state: RootState)` every time
65+
- For `useDispatch`, the default `Dispatch` type does not know about thunks. In order to correctly dispatch thunks, you need to use the specific customized `AppDispatch` type from the store that includes the thunk middleware types, and use that with `useDispatch`. Adding a pre-typed `useDispatch` hook keeps you from forgetting to import `AppDispatch` where it's needed.
66+
67+
Since these are actual variables, not types, it's important to define them in a separate file such as `app/hooks.ts`, not the store setup file. This allows you to import them into any component file that needs to use the hooks, and avoids potential circular import dependency issues.
6168
6269
```ts title="app/hooks.ts"
6370
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
64-
import { RootState, AppDispatch } from './store'
71+
import type { RootState, AppDispatch } from './store'
6572

6673
// highlight-start
6774
// Use throughout your app instead of plain `useDispatch` and `useSelector`
@@ -82,7 +89,7 @@ You can safely import the `RootState` type from the store file here. It's a circ
8289

8390
```ts title="features/counter/counterSlice.ts"
8491
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
85-
import { RootState } from '../../app/store'
92+
import type { RootState } from '../../app/store'
8693

8794
// highlight-start
8895
// Define a type for the slice state
@@ -124,6 +131,17 @@ export const selectCount = (state: RootState) => state.counter.value
124131
export default counterSlice.reducer
125132
```
126133

134+
The generated action creators will be correctly typed to accept a `payload` argument based on the `PayloadAction<T>` type you provided for the reducer. For example, `incrementByAmount` requires a `number` as its argument.
135+
136+
In some cases, [TypeScript may unnecessarily tighten the type of the initial state](https://github.com/reduxjs/redux-toolkit/pull/827). If that happens, you can work around it by casting the initial state using `as`, instead of declaring the type of the variable:
137+
138+
```ts
139+
// Workaround: cast state instead of declaring variable type
140+
const initialState = {
141+
value: 0
142+
} as CounterState
143+
```
144+
127145
### Use Typed Hooks in Components
128146

129147
In component files, import the pre-typed hooks instead of the standard hooks from React-Redux.

docs/usage/immer-reducers.md

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

88
# Writing Reducers with Immer
99

10-
Redux Toolkit's [`createReducer`](../api/createReducer.mdx) and [`createSlice`](../api/createSlice.mdx) automatically use [Immer](https://immerjs.github.io/immer/docs/introduction) internally to let you write simpler immutable update logic using "mutating" syntax. This helps simplify most reducer implementations.
10+
Redux Toolkit's [`createReducer`](../api/createReducer.mdx) and [`createSlice`](../api/createSlice.mdx) automatically use [Immer](https://immerjs.github.io/immer/introduction) internally to let you write simpler immutable update logic using "mutating" syntax. This helps simplify most reducer implementations.
1111

1212
Because Immer is itself an abstraction layer, it's important to understand why Redux Toolkit uses Immer, and how to use it correctly.
1313

@@ -143,7 +143,7 @@ Writing immutable update logic by hand _is_ hard, and **accidentally mutating st
143143

144144
## Immutable Updates with Immer
145145

146-
[Immer](https://immerjs.github.io/immer/docs/introduction) is a library that simplifies the process of writing immutable update logic.
146+
[Immer](https://immerjs.github.io/immer/introduction) is a library that simplifies the process of writing immutable update logic.
147147

148148
Immer provides a function called `produce`, which accepts two arguments: your original `state`, and a callback function. The callback function is given a "draft" version of that state, and inside the callback, it is safe to write code that mutates the draft value. Immer tracks all attempts to mutate the draft value and then replays those mutations using their immutable equivalents to create a safe, immutably updated result:
149149

@@ -363,7 +363,7 @@ It's common to want to log in-progress state from a reducer to see what it looks
363363

364364
![Logged proxy draft](/img/usage/immer-reducers/logged-proxy.png)
365365

366-
To work around this, [Immer includes a `current` function that extracts a copy of the wrapped data](https://immerjs.github.io/immer/docs/current), and RTK re-exports `current`. You can use this in your reducers if you need to log or inspect the work-in-progress state:
366+
To work around this, [Immer includes a `current` function that extracts a copy of the wrapped data](https://immerjs.github.io/immer/current), and RTK re-exports `current`. You can use this in your reducers if you need to log or inspect the work-in-progress state:
367367

368368
```js
369369
import { current } from '@reduxjs/toolkit'
@@ -386,7 +386,7 @@ The correct output would look like this instead:
386386

387387
![Logged current value](/img/usage/immer-reducers/logged-current-state.png)
388388

389-
Immer also provides [`original` and `isDraft` functions](https://immerjs.github.io/immer/docs/original), which retrieves the original data without any updates applied and check to see if a given value is a Proxy-wrapped draft. As of RTK 1.5.0, neither of those is re-exported - you'll need to specifically import them from `immer` yourself. We may re-export them from RTK in an upcoming release.
389+
Immer also provides [`original` and `isDraft` functions](https://immerjs.github.io/immer/original), which retrieves the original data without any updates applied and check to see if a given value is a Proxy-wrapped draft. As of RTK 1.5.0, neither of those is re-exported - you'll need to specifically import them from `immer` yourself. We may re-export them from RTK in an upcoming release.
390390

391391
### Updating Nested Data
392392

@@ -418,7 +418,7 @@ const todosSlice = createSlice({
418418
})
419419
```
420420

421-
There _is_ a gotcha here. [Immer will not wrap objects that are newly inserted into the state](https://immerjs.github.io/immer/docs/pitfalls#data-not-originating-from-the-state-will-never-be-drafted). Most of the time this shouldn't matter, but there may be occasions when you want to insert a value and then make further updates to it.
421+
There _is_ a gotcha here. [Immer will not wrap objects that are newly inserted into the state](https://immerjs.github.io/immer/pitfalls#data-not-originating-from-the-state-will-never-be-drafted). Most of the time this shouldn't matter, but there may be occasions when you want to insert a value and then make further updates to it.
422422

423423
Related to this, RTK's [`createEntityAdapter` update functions](../api/createEntityAdapter.mdx#crud-functions) can either be used as standalone reducers, or "mutating" update functions. These functions determine whether to "mutate" or return a new value by checking to see if the state they're given is wrapped in a draft or not. If you are calling these functions yourself inside of a case reducer, be sure you know whether you're passing them a draft value or a plain value.
424424

@@ -447,6 +447,19 @@ const itemsSlice = createSlice({
447447
})
448448
```
449449

450+
### Linting State Mutations
451+
452+
Many ESLint configs include the https://eslint.org/docs/rules/no-param-reassign rule, which may also warn about mutations to nested fields. That can cause the rule to warn about mutations to `state` in Immer-powered reducers, which is not helpful.
453+
454+
To resolve this, you can tell the ESLint rule to ignore mutations to a parameter named `state`:
455+
456+
```js
457+
{
458+
'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['state'] }]
459+
}
460+
461+
```
462+
450463
## Further Information
451464

452465
See [the Immer documentation](https://immerjs.github.io/immer/) for more details on Immer's APIs, edge cases, and behavior.

docs/usage/usage-with-typescript.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Redux Toolkit is written in TypeScript, and its API is designed to enable great
1919

2020
This page provides specific details for each of the different APIs included in Redux Toolkit and how to type them correctly with TypeScript.
2121

22-
**See the [TypeScript Quick Start tutorial page](../tutorials/typescript.md) for a brief overview of how to set up and use Redux Toolkit and React-Redux to work with TypeScript**.
22+
**See the [TypeScript Quick Start tutorial page](../tutorials/typescript.md) for a brief overview of how to set up and use Redux Toolkit and React Redux to work with TypeScript**.
2323

2424
:::info
2525

@@ -132,9 +132,9 @@ configureStore({
132132
})
133133
```
134134

135-
### Using the extracted `Dispatch` type with React-Redux
135+
### Using the extracted `Dispatch` type with React Redux
136136

137-
By default, the React-Redux `useDispatch` hook does not contain any types that take middlewares into account. If you need a more specific type for the `dispatch` function when dispatching, you may specify the type of the returned `dispatch` function, or create a custom-typed version of `useSelector`. See [the React-Redux documentation](https://react-redux.js.org/using-react-redux/static-typing#typing-the-usedispatch-hook) for details.
137+
By default, the React Redux `useDispatch` hook does not contain any types that take middlewares into account. If you need a more specific type for the `dispatch` function when dispatching, you may specify the type of the returned `dispatch` function, or create a custom-typed version of `useSelector`. See [the React Redux documentation](https://react-redux.js.org/using-react-redux/static-typing#typing-the-usedispatch-hook) for details.
138138

139139
## `createAction`
140140

@@ -166,7 +166,7 @@ createAction('test', withPayloadType<string>())
166166

167167
If you are using `action.type` as a discriminator on a discriminated union, for example to correctly type your payload in `case` statements, you might be interested in this alternative:
168168

169-
Created action creators have a `match` method that acts as a [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates):
169+
Created action creators have a `match` method that acts as a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates):
170170

171171
```typescript
172172
const increment = createAction<number>('increment')
@@ -265,7 +265,7 @@ slice.actions.increment(2)
265265
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })
266266
```
267267

268-
If you have too many reducers and defining them inline would be messy, you can also define them outside the `createSlice` call and type them as `CaseReducer`:
268+
If you have too many case reducers and defining them inline would be messy, or you want to reuse case reducers across slices, you can also define them outside the `createSlice` call and type them as `CaseReducer`:
269269

270270
```typescript
271271
type State = number
@@ -469,17 +469,20 @@ In the most common use cases, you should not need to explicitly declare any type
469469
Just provide a type for the first argument to the `payloadCreator` argument as you would for any function argument, and the resulting thunk will accept the same type as its input parameter.
470470
The return type of the `payloadCreator` will also be reflected in all generated action types.
471471

472-
```ts {8,11,18}
472+
```ts
473473
interface MyData {
474474
// ...
475475
}
476476

477477
const fetchUserById = createAsyncThunk(
478478
'users/fetchById',
479+
// highlight-start
479480
// Declare the type your function argument here:
480481
async (userId: number) => {
482+
// highlight-end
481483
const response = await fetch(`https://reqres.in/api/users/${userId}`)
482484
// Inferred return type: Promise<MyData>
485+
// highlight-next-line
483486
return (await response.json()) as MyData
484487
}
485488
)
@@ -496,17 +499,20 @@ To define the types for these arguments, pass an object as the third generic arg
496499

497500
```ts
498501
const fetchUserById = createAsyncThunk<
502+
// highlight-start
499503
// Return type of the payload creator
500504
MyData,
501505
// First argument to the payload creator
502506
number,
503507
{
508+
// Optional fields for defining thunkApi field types
504509
dispatch: AppDispatch
505510
state: State
506511
extra: {
507512
jwt: string
508513
}
509514
}
515+
// highlight-end
510516
>('users/fetchById', async (userId, thunkApi) => {
511517
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
512518
headers: {
@@ -616,13 +622,14 @@ Typing `createEntityAdapter` only requires you to specify the entity type as the
616622

617623
The example from the `createEntityAdapter` documentation would look like this in TypeScript:
618624

619-
```ts {7}
625+
```ts
620626
interface Book {
621627
bookId: number
622628
title: string
623629
// ...
624630
}
625631

632+
// highlight-next-line
626633
const booksAdapter = createEntityAdapter<Book>({
627634
selectId: book => book.bookId,
628635
sortComparer: (a, b) => a.title.localeCompare(b.title)

0 commit comments

Comments
 (0)