Skip to content

Commit ce3619d

Browse files
jherrmarkerikson
authored andcommitted
PR review fixes and per-route state
1 parent 9897710 commit ce3619d

File tree

2 files changed

+148
-26
lines changed

2 files changed

+148
-26
lines changed

docs/tutorials/nextjs.mdx

Lines changed: 147 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@ hide_title: true
2626

2727
## Introduction
2828

29-
NextJS is a server side rendering framework for React that presents some unique challenges for using Redux properly. There are two architectures for a NextJS application; the Pages Router and the App Router. The Pages Router is the original architecture for NextJS. Using Redux with the Pages Router is well understood and handled primarily by the (next-redux-wrapper)[https://github.com/kirill-konshin/next-redux-wrapper]. This tutorial will focus on the App Router architecture as it is the new default architecture option for NextJS and it presents some unique challenges for using Redux properly.
29+
NextJS is a server side rendering framework for React that presents some unique challenges for using Redux properly.
30+
These challenges include:
31+
32+
- **Per-request safe Redux store creation** - A NextJS server can handle multiple requests simultaneously. This means that the Redux store should be created per request and that the store should not be shared across requests.
33+
- **SSR-friendly store hydration** - NextJS applications are rendered twice, first on the server and again on the client. Failure to render the same page on both the client and the server will result in a "hydration error". So the Redux store will have to be initialized on the server and then re-initialized on the client with the same data.
34+
- **SPA routing support** - NextJS supports a hybrid model for client side routing. A customers first page load will get an SSR result from the server. Subsequent page loads will be handled by the client. This means that the Redux store should be preserved appropriately when navigating from route to route using NextJS's client side routing.
35+
- **Server caching friendly** - Recent versions of NextJS (specifically applications using the App Router architecture) support aggressive server caching. The ideal store artchiecture should support this caching.
36+
37+
There are two architectures for a NextJS application; the Pages Router and the App Router. The Pages Router is the original architecture for NextJS.
38+
39+
Using Redux with the Pages Router is well understood and handled primarily by the (next-redux-wrapper)[https://github.com/kirill-konshin/next-redux-wrapper]. This tutorial will focus on the App Router architecture as it is the new default architecture option for NextJS and it presents some unique challenges for using Redux properly.
3040

3141
### How to Read This Tutorial
3242

@@ -38,54 +48,70 @@ The primary new feature of the NextJS App Router is the addition of support for
3848

3949
RSCs abilitiy to block for data means that with the App Router you no longer have `getServerSideProps` to fetch data for rendering. Any component in the tree can make asychronous requests for data. While this is very convenient it also means thats if you define global variables (like the Redux store) they will be shared across requests. This is a problem because the Redux store could be contaminated with data from other requests.
4050

51+
Based on the architecture of the App Router we have these general recommendations for appropriate use of Redux:
52+
53+
- **No global stores** - Because the Redux store is shared across requests, it should not be defined as a global variable. Instead, the store should be created per request.
54+
- **RSCs should not read or write the Redux store** - RSCs cannot use hooks or context. They aren't meant to be stateful. Having an RSC read or write values from a global store violates the architecture of the NextJS App Router.
55+
- **The store should only contain mutable data** - We recommend that you use your Redux sparingly for data intended to be global and mutable.
56+
4157
### Creating a Redux Store per Request
4258

43-
Following along with the (Quick-Start guide)[./quick-start.mdx] we need to make some changes to the `app/store.js` file. The first change is to move from defining store as a global to defining a `createStore` function that returns a new store for each request.
59+
Following along with the (Quick-Start guide)[./quick-start.mdx] we need to make some changes to the `app/store.js` file. The first change is to move from defining store as a global to defining a `makeStore` function that returns a new store for each request.
4460

4561
```ts title="src/app/store.ts"
4662
import { configureStore } from '@reduxjs/toolkit'
4763

48-
export const createStore = () =>
64+
export const makeStore = () =>
4965
configureStore({
5066
reducer: {},
5167
})
5268

53-
// Infer the type of createStore
54-
export type StoreType = ReturnType<typeof createStore>
69+
// Infer the type of makeStore
70+
export type StoreType = ReturnType<typeof makeStore>
5571
// Infer the `RootState` and `AppDispatch` types from the store itself
5672
export type RootState = ReturnType<StoreType['getState']>
5773
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
5874
export type AppDispatch = StoreType['dispatch']
5975
```
6076
61-
To use this new `createStore` function we need to create a new "client" component that will create the store and share it using the React-Redux `Provider` component.
77+
Now we have a function, `makeStore`, that we can use to create a store instance per-request while retaining the strong type safety (if you choose to use TypeScript) that Redux Toolkit provides.
78+
79+
To use this new `makeStore` function we need to create a new "client" component that will create the store and share it using the React-Redux `Provider` component.
80+
81+
:::tip Why Client Components?
82+
83+
Any component that interacts with the Redux store; creating it, providing it, reading from it, or writing to it, needs to be a client component because accessing the store requires React context and context is only available in client components.
84+
85+
:::
6286
6387
```ts title="src/app/StoreProvider.tsx"
6488
// file: app/store.ts noEmit
6589
import { configureStore } from '@reduxjs/toolkit'
6690

6791
// highlight-start
68-
export const createStore = () =>
92+
export const makeStore = () =>
6993
configureStore({
7094
reducer: {},
7195
})
7296
// highlight-end
7397

74-
// Infer the type of createStore
98+
// Infer the type of makeStore
7599
// highlight-start
76-
export type StoreType = ReturnType<typeof createStore>
100+
export type StoreType = ReturnType<typeof makeStore>
77101
// highlight-end
78102
// Infer the `RootState` and `AppDispatch` types from the store itself
79103
export type RootState = ReturnType<StoreType['getState']>
80104
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
81105
export type AppDispatch = StoreType['dispatch']
82106

107+
/* prettier-ignore */
108+
83109
// file: app/StoreProvider.tsx
84-
;('use client')
110+
'use client'
85111
import { useRef } from 'react'
86112
import { Provider } from 'react-redux'
87113
// highlight-start
88-
import { createStore } from './store'
114+
import { makeStore, StoreType } from './store'
89115
// highlight-end
90116

91117
export default function StoreProvider({
@@ -94,7 +120,7 @@ export default function StoreProvider({
94120
children: React.ReactNode
95121
}) {
96122
// highlight-start
97-
const storeRef = useRef<ReturnType<typeof createStore>>(createStore())
123+
const storeRef = useRef<StoreType>(makeStore())
98124
// highlight-end
99125

100126
return <Provider store={storeRef.current}>{children}</Provider>
@@ -114,40 +140,42 @@ const counterSlice = createSlice({
114140
value: 0,
115141
},
116142
reducers: {
117-
setCount: (state, action: PayloadAction<number>) => {
143+
initializeCount: (state, action: PayloadAction<number>) => {
118144
state.value = action.payload
119145
},
120146
},
121147
})
122148

123-
export const { setCount } = counterSlice.actions
149+
export const { initializeCount } = counterSlice.actions
124150
export default counterSlice.reducer
125151

126152
// file: app/store.ts noEmit
127153
import { configureStore } from '@reduxjs/toolkit'
128154
import counterReducer from '../features/counter/counterSlice'
129155

130-
export const createStore = () =>
156+
export const makeStore = () =>
131157
configureStore({
132158
reducer: {
133159
counter: counterReducer,
134160
},
135161
})
136162

137-
// Infer the type of createStore
138-
export type StoreType = ReturnType<typeof createStore>
163+
// Infer the type of makeStore
164+
export type StoreType = ReturnType<typeof makeStore>
139165
// Infer the `RootState` and `AppDispatch` types from the store itself
140166
export type RootState = ReturnType<StoreType['getState']>
141167
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
142168
export type AppDispatch = StoreType['dispatch']
143169

170+
/* prettier-ignore */
171+
144172
// file: app/StoreProvider.tsx
145-
;('use client')
173+
'use client'
146174
import { useRef } from 'react'
147175
import { Provider } from 'react-redux'
148-
import { createStore } from './store'
176+
import { makeStore, StoreType } from './store'
149177
// highlight-start
150-
import { setCount } from '../features/counter/counterSlice'
178+
import { initializeCount } from '../features/counter/counterSlice'
151179
// highlight-end
152180

153181
export default function StoreProvider({
@@ -157,29 +185,123 @@ export default function StoreProvider({
157185
count: number
158186
children: React.ReactNode
159187
}) {
160-
const storeRef = useRef<ReturnType<typeof createStore> | null>(null)
188+
const storeRef = useRef<StoreType | null>(null)
161189
if (!storeRef.current) {
162-
storeRef.current = createStore()
190+
storeRef.current = makeStore()
163191
// highlight-start
164-
storeRef.current.dispatch(setCount(count))
192+
storeRef.current.dispatch(initializeCount(count))
165193
// highlight-end
166194
}
167195

168196
return <Provider store={storeRef.current}>{children}</Provider>
169197
}
170198
```
171199

172-
The next step is to include the `StoreProvider` in your layout component. This will ensure that the store is created for each request and that the store is not shared across requests. Use the store exactly as you would normally using the hooks provided by `react-redux`.
200+
In this example code we are insuring that this client component is re-render safe by checking the value of the reference. This component will only be rendered once per request on the server but might be rendered multiple times on the client if there are stateful client components located above this component in the tree, or if this component also contains other mutable state that causes a re-render.
201+
202+
The next step is to include the `StoreProvider` in your layout component. This will ensure that the store is created for each request and that the store is not shared across requests. In all client components further down the tree, you can use the store exactly as you would normally using the hooks provided by `react-redux`.
203+
204+
### Per-route state
205+
206+
If you use NextJS's support for client side SPA-style navigation by using `next/navigation` then when customers navigating from page to page only the route component will be re-rendered. This means that if you have a Redux store created and provided in the layout component it will be preserved across route changes. This is not a problem if you are only using the store for global, mutable data. However, if you are using the store for per-route data then you will need to reset the store when the route changes.
207+
208+
Shown below is a `ProductName` example component that uses the Redux store to store the mutable name of a product. The `ProductName` component part of a product detail route. In order to ensure that we have the correct name in the store we need to set the value in the store any time the `ProductName` component is initially rendered, which happens on any route change to the product detail route.
209+
210+
```ts title="src/app/ProductName.tsx"
211+
// file: features/product/productSlice.ts noEmit
212+
import { createSlice } from '@reduxjs/toolkit'
213+
import type { PayloadAction } from '@reduxjs/toolkit'
214+
215+
export interface Product {
216+
name: string
217+
}
218+
219+
const productSlice = createSlice({
220+
name: 'product',
221+
initialState: {
222+
name: '',
223+
},
224+
reducers: {
225+
initializeProduct: (state, action: PayloadAction<Product>) => {
226+
state.name = action.payload.name
227+
},
228+
setProductName: (state, action: PayloadAction<string>) => {
229+
state.name = action.payload
230+
},
231+
},
232+
})
233+
234+
export const { initializeProduct, setProductName } = productSlice.actions
235+
export default productSlice.reducer
236+
237+
// file: app/store.ts noEmit
238+
import { configureStore } from '@reduxjs/toolkit'
239+
import productReducer from '../features/product/productSlice'
240+
241+
export const makeStore = () =>
242+
configureStore({
243+
reducer: {
244+
product: productReducer,
245+
},
246+
})
247+
248+
// Infer the type of makeStore
249+
export type StoreType = ReturnType<typeof makeStore>
250+
// Infer the `RootState` and `AppDispatch` types from the store itself
251+
export type RootState = ReturnType<StoreType['getState']>
252+
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
253+
export type AppDispatch = StoreType['dispatch']
254+
255+
/* prettier-ignore */
256+
257+
// file: app/ProductName.tsx
258+
'use client'
259+
import { useRef } from 'react'
260+
import { Provider, useStore, useSelector, useDispatch } from 'react-redux'
261+
import { makeStore, RootState } from './store'
262+
import {
263+
initializeProduct,
264+
setProductName,
265+
Product,
266+
} from '../features/product/productSlice'
267+
268+
export default function ProductName({ product }: { product: Product }) {
269+
// highlight-start
270+
// Initialize the store with the product information
271+
const store = useStore<RootState>()
272+
const initialized = useRef(false)
273+
if (!initialized.current) {
274+
store.dispatch(initializeProduct(product))
275+
initialized.current = true
276+
}
277+
const name = useSelector((state: RootState) => state.product.name)
278+
// highlight-end
279+
const dispatch = useDispatch()
280+
281+
return (
282+
<input
283+
value={name}
284+
onChange={(e) => dispatch(setProductName(e.target.value))}
285+
/>
286+
)
287+
}
288+
```
289+
290+
As we did we initializing the store on creation using dispatched actions we can also update the per-route store state to the new route state using dispatched actions. The `initialized` ref is used to ensure that the store is only initialized once per request.
291+
292+
It is worth noting that initializing the store with a `useEffect` would not work because `useEffect` only runs on the client. This would result in hydration errors or flicker because the result from a server side render would not match the result from the client side render.
173293

174294
## What You've Learned
175295

176296
That was a brief overview of how to set up and use Redux Toolkit with the App Router:
177297

178298
:::tip Summary
179299

180-
- **Create a Redux store per request by using `configureStore` wrapped in a `createStore` function**
300+
- **Create a Redux store per request by using `configureStore` wrapped in a `makeStore` function**
181301
- **Provide the Redux store to the React application components** using a "client" component
302+
- **Only interact with the Redux store in client components** because only client components have access to React context
182303
- **Use the store as you normally would using the hooks provided in react-redux**
304+
- **You need to account for the case where you have per-route state in a global store located in the layout**
183305

184306
## What's Next?
185307

docs/tutorials/quick-start.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ hide_title: true
77

88
&nbsp;
99

10-
# NextJS Integration Tutorial
10+
# Redux Toolkit Quick Start
1111

1212
:::tip What You'll Learn
1313

0 commit comments

Comments
 (0)