diff --git a/docs/tutorials/essentials/part-1-overview-concepts.md b/docs/tutorials/essentials/part-1-overview-concepts.md
index 32614948e6..6b4233b1d9 100644
--- a/docs/tutorials/essentials/part-1-overview-concepts.md
+++ b/docs/tutorials/essentials/part-1-overview-concepts.md
@@ -19,13 +19,13 @@ import { DetailedExplanation } from '../../components/DetailedExplanation'
Welcome to the Redux Essentials tutorial! **This tutorial will introduce you to Redux and teach you how to use it the right way, using our latest recommended tools and best practices**. By the time you finish, you should be able to start building your own Redux applications using the tools and patterns you've learned here.
-In Part 1 of this tutorial, we'll cover the key concepts and terms you need to know to use Redux, and in [Part 2: Redux App Structure](./part-2-app-structure.md) we'll examine a basic React + Redux app to see how the pieces fit together.
+In Part 1 of this tutorial, we'll cover the key concepts and terms you need to know to use Redux, and in [Part 2: Redux App Structure](./part-2-app-structure.md) we'll examine a typical React + Redux app to see how the pieces fit together.
Starting in [Part 3: Basic Redux Data Flow](./part-3-data-flow.md), we'll use that knowledge to build a small social media feed app with some real-world features, see how those pieces actually work in practice, and talk about some important patterns and guidelines for using Redux.
### How to Read This Tutorial
-This page will focus on showing you _how_ to use Redux the right way, and explain just enough of the concepts so that you can understand how to build Redux apps correctly.
+This tutorial focuses on showing you _how_ to use Redux the right way, and explains concepts along the way so that you can understand how to build Redux apps correctly.
We've tried to keep these explanations beginner-friendly, but we do need to make some assumptions about what you know already:
@@ -33,14 +33,15 @@ We've tried to keep these explanations beginner-friendly, but we do need to make
- Familiarity with [HTML & CSS](https://internetingishard.netlify.app/html-and-css/index.html).
- Familiarity with [ES2015 syntax and features](https://www.taniarascia.com/es6-syntax-and-feature-overview/)
-- Knowledge of React terminology: [JSX](https://react.dev/learn/writing-markup-with-jsx), [State](https://react.dev/learn/state-a-components-memory), [Function Components](https://react.dev/learn/your-first-component), [Props](https://react.dev/learn/passing-props-to-a-component), and [Hooks](https://react.dev/reference/react)
-- Knowledge of [asynchronous JavaScript](https://javascript.info/promise-basics) and [making AJAX requests](https://javascript.info/fetch)
+- Knowledge of React terminology: [JSX](https://react.dev/learn/writing-markup-with-jsx), [Function Components](https://react.dev/learn/your-first-component), [Props](https://react.dev/learn/passing-props-to-a-component), [State](https://react.dev/learn/state-a-components-memory), and [Hooks](https://react.dev/reference/react)
+- Knowledge of [asynchronous JavaScript](https://javascript.info/promise-basics) and [making HTTP requests](https://javascript.info/fetch)
+- Basic understanding of [TypeScript syntax and usage](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html)
:::
**If you're not already comfortable with those topics, we encourage you to take some time to become comfortable with them first, and then come back to learn about Redux**. We'll be here when you're ready!
-You should make sure that you have the React and Redux DevTools extensions installed in your browser:
+You should also make sure that you have the React and Redux DevTools extensions installed in your browser:
- React DevTools Extension:
- [React DevTools Extension for Chrome](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)
@@ -53,7 +54,7 @@ You should make sure that you have the React and Redux DevTools extensions insta
It helps to understand what this "Redux" thing is in the first place. What does it do? What problems does it help me solve? Why would I want to use it?
-**Redux is a pattern and library for managing and updating application state, using events called "actions".** It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
+**Redux is a pattern and library for managing and updating global application state, where the UI triggers events called "actions" to describe what happened, and separate update logic called "reducers" updates the state in response.** It serves as a centralized store for state that needs to be used across your entire application, with rules ensuring that the state can only be updated in a predictable fashion.
### Why Should I Use Redux?
@@ -87,16 +88,16 @@ If you're not sure whether Redux is a good choice for your app, these resources
### Redux Libraries and Tools
-Redux is a small standalone JS library. However, it is commonly used with several other packages:
-
-#### React-Redux
-
-Redux can integrate with any UI framework, and is most frequently used with React. [**React-Redux**](https://react-redux.js.org/) is our official package that lets your React components interact with a Redux store by reading pieces of state and dispatching actions to update the store.
+Redux at its core is a small standalone JS library. It is commonly used with several other packages:
#### Redux Toolkit
[**Redux Toolkit**](https://redux-toolkit.js.org) is our recommended approach for writing Redux logic. It contains packages and functions that we think are essential for building a Redux app. Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications.
+#### React-Redux
+
+Redux can integrate with any UI framework, and is most frequently used with React. [**React-Redux**](https://react-redux.js.org/) is our official package that lets your React components interact with a Redux store by reading pieces of state and dispatching actions to update the store.
+
#### Redux DevTools Extension
The [**Redux DevTools Extension**](https://github.com/reduxjs/redux-devtools/tree/main/extension) shows a history of the changes to the state in your Redux store over time. This allows you to debug your applications effectively, including using powerful techniques like "time-travel debugging".
@@ -205,7 +206,7 @@ const arr3 = arr.slice()
arr3.push('c')
```
-**Redux expects that all state updates are done immutably**. We'll look at where and how this is important a bit later, as well as some easier ways to write immutable update logic.
+**React and Redux expect that all state updates are done immutably**. We'll look at where and how this is important a bit later, as well as some easier ways to write immutable update logic.
:::info Want to Know More?
@@ -264,7 +265,7 @@ Reducers must _always_ follow some specific rules:
- They should only calculate the new state value based on the `state` and `action` arguments
- They are not allowed to modify the existing `state`. Instead, they must make _immutable updates_, by copying the existing `state` and making changes to the copied values.
-- They must not do any asynchronous logic, calculate random values, or cause other "side effects"
+- They must be "pure" - they cannot do any asynchronous logic, calculate random values, or cause other "side effects"
We'll talk more about the rules of reducers later, including why they're important and how to follow them correctly.
@@ -442,7 +443,11 @@ Redux does have a number of new terms and concepts to remember. As a reminder, h
- **Redux is a library for managing global application state**
- Redux is typically used with the React-Redux library for integrating Redux and React together
- - Redux Toolkit is the recommended way to write Redux logic
+ - Redux Toolkit is the standard way to write Redux logic
+- **Redux's update pattern separates "what happened" from "how the state changes"**
+ - _Actions_ are plain objects with a `type` field, and describe "what happened" in the app
+ - _Reducers_ are functions that calculate a new state value based on previous state + an action
+ - A Redux _store_ runs the root reducer whenever an action is _dispatched_
- **Redux uses a "one-way data flow" app structure**
- State describes the condition of the app at a point in time, and UI renders based on that state
- When something happens in the app:
@@ -450,10 +455,6 @@ Redux does have a number of new terms and concepts to remember. As a reminder, h
- The store runs the reducers, and the state is updated based on what occurred
- The store notifies the UI that the state has changed
- The UI re-renders based on the new state
-- **Redux uses several types of code**
- - _Actions_ are plain objects with a `type` field, and describe "what happened" in the app
- - _Reducers_ are functions that calculate a new state value based on previous state + an action
- - A Redux _store_ runs the root reducer whenever an action is _dispatched_
:::
diff --git a/docs/tutorials/essentials/part-2-app-structure.md b/docs/tutorials/essentials/part-2-app-structure.md
index b3b5a6b761..5df4d77548 100644
--- a/docs/tutorials/essentials/part-2-app-structure.md
+++ b/docs/tutorials/essentials/part-2-app-structure.md
@@ -24,22 +24,28 @@ Now, let's look at a real working example to see how these pieces fit together.
The sample project we'll look at is a small counter application that lets us add or subtract from a number as we click buttons. It may not be very exciting, but it shows all the important pieces of a React+Redux application in action.
-The project has been created using [the official Redux template for Create-React-App](https://github.com/reduxjs/redux-templates). Out of the box, it has already been configured with a standard Redux application structure, using [Redux Toolkit](https://redux-toolkit.js.org) to create the Redux store and logic, and [React-Redux](https://react-redux.js.org) to connect together the Redux store and the React components.
+The project has been created using a smaller version of [the official Redux Toolkit template for Vite](https://github.com/reduxjs/redux-templates/tree/master/packages/vite-template-redux). Out of the box, it has already been configured with a standard Redux application structure, using [Redux Toolkit](https://redux-toolkit.js.org) to create the Redux store and logic, and [React-Redux](https://react-redux.js.org) to connect together the Redux store and the React components.
Here's the live version of the project. You can play around with it by clicking the buttons in the app preview on the right, and browse through the source files on the left.
-If you'd like to try create this project on your own computer, you can [start a new Create-React-App project](https://create-react-app.dev/docs/getting-started#selecting-a-template) using our Redux template:
+If you'd like to set up this project on your own computer, you can create a local copy with this command:
+```sh
+npx degit reduxjs/redux-templates/packages/rtk-app-structure-example my-app
```
-npx create-react-app redux-essentials-example --template redux
+
+You can also create a new project using the full Redux Toolkit template for Vite:
+
+```sh
+npx degit reduxjs/redux-templates/packages/vite-template-redux my-app
```
### Using the Counter App
@@ -48,7 +54,7 @@ The counter app has already been set up to let us watch what happens inside as w
Open up your browser's DevTools. Then, choose the "Redux" tab in the DevTools, and click the "State" button in the upper-right toolbar. You should see something that looks like this:
-![Redux DevTools: initial app state](/img/tutorials/essentials/devtools-initial.png)
+![Redux DevTools: initial app state](/img/tutorials/essentials/devtools-basic-counter.png)
On the right, we can see that our Redux store is starting off with an app state value that looks like this:
@@ -56,6 +62,7 @@ On the right, we can see that our Redux store is starting off with an app state
{
counter: {
value: 0
+ status: 'idle'
}
}
```
@@ -109,55 +116,72 @@ Now that you know what the app does, let's look at how it works.
Here are the key files that make up this application:
- `/src`
- - `index.js`: the starting point for the app
- - `App.js`: the top-level React component
+ - `main.tsx`: the starting point for the app
+ - `App.tsx`: the top-level React component
- `/app`
- - `store.js`: creates the Redux store instance
+ - `store.ts`: creates the Redux store instance
+ - `hooks.ts`: exports pre-typed React-Redux hooks
- `/features`
- `/counter`
- - `Counter.js`: a React component that shows the UI for the counter feature
- - `counterSlice.js`: the Redux logic for the counter feature
+ - `Counter.tsx`: a React component that shows the UI for the counter feature
+ - `counterSlice.ts`: the Redux logic for the counter feature
Let's start by looking at how the Redux store is created.
-### Creating the Redux Store
+## Creating the Redux Store
-Open up `app/store.js`, which should look like this:
+Open up `app/store.ts`, which should look like this:
-```js title="app/store.js"
+```ts title="app/store.ts"
+import type { Action, ThunkAction } from '@reduxjs/toolkit'
import { configureStore } from '@reduxjs/toolkit'
-import counterReducer from '../features/counter/counterSlice'
+import counterReducer from '@/features/counter/counterSlice'
-export default configureStore({
+export const store = configureStore({
reducer: {
counter: counterReducer
}
})
+
+// Infer the type of `store`
+export type AppStore = typeof store
+export type RootState = ReturnType
+// Infer the `AppDispatch` type from the store itself
+export type AppDispatch = AppStore['dispatch']
+// Define a reusable type describing thunk functions
+export type AppThunk = ThunkAction<
+ ThunkReturnType,
+ RootState,
+ unknown,
+ Action
+>
```
The Redux store is created using the `configureStore` function from Redux Toolkit. `configureStore` requires that we pass in a `reducer` argument.
Our application might be made up of many different features, and each of those features might have its own reducer function. When we call `configureStore`, we can pass in all of the different reducers in an object. The key names in the object will define the keys in our final state value.
-We have a file named `features/counter/counterSlice.js` that exports a reducer function for the counter logic. We can import that `counterReducer` function here, and include it when we create the store.
+We have a file named `features/counter/counterSlice.ts` that exports a reducer function for the counter logic. We can import that `counterReducer` function here, and include it when we create the store.
When we pass in an object like `{counter: counterReducer}`, that says that we want to have a `state.counter` section of our Redux state object, and that we want the `counterReducer` function to be in charge of deciding if and how to update the `state.counter` section whenever an action is dispatched.
Redux allows store setup to be customized with different kinds of plugins ("middleware" and "enhancers"). `configureStore` automatically adds several middleware to the store setup by default to provide a good developer experience, and also sets up the store so that the Redux DevTools Extension can inspect its contents.
-#### Redux Slices
+For TypeScript usage, we also want to export some reusable types based on the Store, such as the `RootState` and `AppDispatch` types. We'll see how those get used later.
+
+## Redux Slices
**A "slice" is a collection of Redux reducer logic and actions for a single feature in your app**, typically defined together in a single file. The name comes from splitting up the root Redux state object into multiple "slices" of state.
For example, in a blogging app, our store setup might look like:
-```js
+```ts
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'
-export default configureStore({
+export const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
@@ -166,7 +190,7 @@ export default configureStore({
})
```
-In that example, `state.users`, `state.posts`, and `state.comments` are each a separate "slice" of the Redux state. Since `usersReducer` is responsible for updating the `state.users` slice, we refer to it as a "slice reducer" function.
+In that example, `state.users`, `state.posts`, and `state.comments` are each a separate "slice" of the Redux state. Since `usersReducer` is responsible for updating the `state.users` slice, we refer to it as a **"slice reducer" function**.
@@ -210,20 +234,34 @@ const store = configureStore({
### Creating Slice Reducers and Actions
-Since we know that the `counterReducer` function is coming from `features/counter/counterSlice.js`, let's see what's in that file, piece by piece.
+Since we know that the `counterReducer` function is coming from `features/counter/counterSlice.ts`, let's see what's in that file, piece by piece.
-```js title="features/counter/counterSlice.js"
-import { createSlice } from '@reduxjs/toolkit'
+```ts title="features/counter/counterSlice.ts"
+import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
+import type { PayloadAction } from '@reduxjs/toolkit'
+// Define the TS type for the counter slice's state
+export interface CounterState {
+ value: number
+ status: 'idle' | 'loading' | 'failed'
+}
+
+// Define the initial value for the slice state
+const initialState: CounterState = {
+ value: 0,
+ status: 'idle'
+}
+
+// Slices contain Redux reducer logic for updating state, and
+// generate actions that can be dispatched to trigger those updates.
export const counterSlice = createSlice({
name: 'counter',
- initialState: {
- value: 0
- },
+ initialState,
+ // The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
- // doesn't actually mutate the state because it uses the immer library,
+ // doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
@@ -231,14 +269,17 @@ export const counterSlice = createSlice({
decrement: state => {
state.value -= 1
},
- incrementByAmount: (state, action) => {
+ // Use the PayloadAction type to declare the contents of `action.payload`
+ incrementByAmount: (state, action: PayloadAction) => {
state.value += action.payload
}
}
})
+// Export the generated action creators for use in components
export const { increment, decrement, incrementByAmount } = counterSlice.actions
+// Export the slice reducer for use in the store configuration
export default counterSlice.reducer
```
@@ -252,9 +293,9 @@ We know that actions are plain objects with a `type` field, the `type` field is
We _could_ write those all by hand, every time. But, that would be tedious. Besides, what's _really_ important in Redux is the reducer functions, and the logic they have for calculating new state.
-Redux Toolkit has a function called `createSlice`, which takes care of the work of generating action type strings, action creator functions, and action objects. All you have to do is define a name for this slice, write an object that has some reducer functions in it, and it generates the corresponding action code automatically. The string from the `name` option is used as the first part of each action type, and the key name of each reducer function is used as the second part. So, the `"counter"` name + the `"increment"` reducer function generated an action type of `{type: "counter/increment"}`. (After all, why write this by hand if the computer can do it for us!)
+Redux Toolkit has a function called [**`createSlice`**](https://redux-toolkit.js.org/api/createSlice), which takes care of the work of generating action type strings, action creator functions, and action objects. All you have to do is define a name for this slice, write an object that has some reducer functions in it, and it generates the corresponding action code automatically. The string from the `name` option is used as the first part of each action type, and the key name of each reducer function is used as the second part. So, the `"counter"` name + the `"increment"` reducer function generated an action type of `{type: "counter/increment"}`. (After all, why write this by hand if the computer can do it for us!)
-In addition to the `name` field, `createSlice` needs us to pass in the initial state value for the reducers, so that there is a `state` the first time it gets called. In this case, we're providing an object with a `value` field that starts off at 0.
+In addition to the `name` field, `createSlice` needs us to pass in the initial state value for the reducers, so that there is a `state` the first time it gets called. In this case, we're providing an object with a `value` field that starts off at 0, and a `status` field that starts off with `'idle'`.
We can see here that there are three reducer functions, and that corresponds to the three different action types that were dispatched by clicking the different buttons.
@@ -276,13 +317,13 @@ console.log(newState)
// {value: 11}
```
-### Rules of Reducers
+## Rules of Reducers
We said earlier that reducers must **always** follow some special rules:
- They should only calculate the new state value based on the `state` and `action` arguments
- They are not allowed to modify the existing `state`. Instead, they must make _immutable updates_, by copying the existing `state` and making changes to the copied values.
-- They must not do any asynchronous logic or other "side effects"
+- They must be "pure" - they cannot do any asynchronous logic or other "side effects"
But why are these rules important? There are a few different reasons:
@@ -374,22 +415,21 @@ But, here's something _very_ important to remember:
:::warning
-**You can _only_ write "mutating" logic in Redux Toolkit's `createSlice` and `createReducer` because they use Immer inside! If you write mutating logic in reducers without Immer, it _will_ mutate the state and cause bugs!**
+**You can _only_ write "mutating" logic in Redux Toolkit's `createSlice` and `createReducer` because they use Immer inside! If you write mutating logic in your code without Immer, it _will_ mutate the state and cause bugs!**
:::
With that in mind, let's go back and look at the actual reducers from the counter slice.
-```js title="features/counter/counterSlice.js"
+```ts title="features/counter/counterSlice.ts"
export const counterSlice = createSlice({
name: 'counter',
- initialState: {
- value: 0
- },
+ initialState,
+ // The `reducers` field lets us define reducers and generate associated actions
reducers: {
increment: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
- // doesn't actually mutate the state because it uses the immer library,
+ // doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
state.value += 1
@@ -397,7 +437,8 @@ export const counterSlice = createSlice({
decrement: state => {
state.value -= 1
},
- incrementByAmount: (state, action) => {
+ // Use the PayloadAction type to declare the contents of `action.payload`
+ incrementByAmount: (state, action: PayloadAction) => {
// highlight-next-line
state.value += action.payload
}
@@ -409,12 +450,48 @@ We can see that the `increment` reducer will always add 1 to `state.value`. Beca
In both of those reducers, we don't actually need to have our code look at the `action` object. It will be passed in anyway, but since we don't need it, we can skip declaring `action` as a parameter for the reducers.
-On the other hand, the `incrementByAmount` reducer _does_ need to know something: how much it should be adding to the counter value. So, we declare the reducer as having both `state` and `action` arguments. In this case, we know that the amount we typed into the textbox is being put into the `action.payload` field, so we can add that to `state.value`.
+On the other hand, the `incrementByAmount` reducer _does_ need to know something: how much it should be adding to the counter value. So, we declare the reducer as having both `state` and `action` arguments. In this case, we know that the amount we typed into the "amount" input is being put into the `action.payload` field, so we can add that to `state.value`.
+
+If we're using TypeScript, we need to tell TS what the type of `action.payload` will be. The `PayloadAction` type declares that "this is an action object, where the type of `action.payload` is..." whatever type you supplied. In this case, we know that the UI has taken the numeric string that was typed into the "amount" textbox, converted it into a number, and is trying to dispatch the action with that value, so we'll declare that this is `action: PayloadAction`.
:::info Want to Know More?
For more information on immutability and writing immutable updates, see [the "Immutable Update Patterns" docs page](../../usage/structuring-reducers/ImmutableUpdatePatterns.md) and [The Complete Guide to Immutability in React and Redux](https://daveceddia.com/react-redux-immutability-guide/).
+For details on using Immer for "mutating" immutable updates, see [the Immer docs](https://immerjs.github.io/immer/) and the ["Writing Reducers with Immer" docs page](https://redux-toolkit.js.org/usage/immer-reducers).
+
+:::
+
+## Additional Redux Logic
+
+The core of Redux is reducers, actions, and the store. There's a couple additional types of Redux functions that are commonly used as well.
+
+### Reading Data with Selectors
+
+We can call `store.getState()` to get the entire current root state object, and access its fields like `state.counter.value`.
+
+It's standard to write "selector" functions that do those state field lookups for us. In this case, `counterSlice.ts` exports two selector functions that can be reused:
+
+```ts
+// Selector functions allows us to select a value from the Redux root state.
+// Selectors can also be defined inline in the `useSelector` call
+// in a component, or inside the `createSlice.selectors` field.
+export const selectCount = (state: RootState) => state.counter.value
+export const selectStatus = (state: RootState) => state.counter.status
+```
+
+Selector functions are normally called with the entire Redux root state object as an argument. They can read out specific values from the root state, or do calculations and return new values.
+
+Since we're using TypeScript, we also need to use the `RootState` type that was exported from `store.ts` to define the type of the `state` argument in each selector.
+
+Note that you **don't have to create separate selector functions for every field in every slice!** (This particular example did, to show off the idea of writing selectors, but we only had two fields in `counterSlice.ts` anyway) Instead, [find a balance in how many selectors you write](../../usage/deriving-data-selectors.md#balance-selector-usage)
+
+:::info More Info on Selectors
+
+We'll learn more about selector functions in [Part 4: Using Redux Data](./part-4-using-data.md#reading-data-with-selectors), and look at how they can be optimized in [Part 6: Performance](./part-6-performance-normalization.md#memoizing-selector-functions)
+
+See [Deriving Data with Selectors](../../usage/deriving-data-selectors.md) for a longer look at why and how to use selector functions.
+
:::
### Writing Async Logic with Thunks
@@ -423,39 +500,47 @@ So far, all the logic in our application has been synchronous. Actions are dispa
A **thunk** is a specific kind of Redux function that can contain asynchronous logic. Thunks are written using two functions:
-- An inside thunk function, which gets `dispatch` and `getState` as arguments
-- The outside creator function, which creates and returns the thunk function
+- An inner thunk function, which gets `dispatch` and `getState` as arguments
+- The outer creator function, which creates and returns the thunk function
The next function that's exported from `counterSlice` is an example of a thunk action creator:
-```js title="features/counter/counterSlice.js"
-// The function below is called a thunk and allows us to perform async logic.
-// It can be dispatched like a regular action: `dispatch(incrementAsync(10))`.
-// This will call the thunk with the `dispatch` function as the first argument.
-// Async code can then be executed and other actions can be dispatched
-export const incrementAsync = amount => dispatch => {
- setTimeout(() => {
- dispatch(incrementByAmount(amount))
- }, 1000)
+```ts title="features/counter/counterSlice.ts"
+// The function below is called a thunk, which can contain both sync and async logic
+// that has access to both `dispatch` and `getState`. They can be dispatched like
+// a regular action: `dispatch(incrementIfOdd(10))`.
+// Here's an example of conditionally dispatching actions based on current state.
+export const incrementIfOdd = (amount: number): AppThunk => {
+ return (dispatch, getState) => {
+ const currentValue = selectCount(getState())
+ if (currentValue % 2 === 1) {
+ dispatch(incrementByAmount(amount))
+ }
+ }
}
```
+In this thunk, we use `getState()` to get the store's current root state value, and `dispatch()` to dispatch another action. We could easily put async logic here as well, such as a `setTimeout` or an `await`.
+
We can use them the same way we use a typical Redux action creator:
-```js
-store.dispatch(incrementAsync(5))
+```ts
+store.dispatch(incrementIfOdd(6))
```
-However, using thunks requires that the `redux-thunk` _middleware_ (a type of plugin for Redux) be added to the Redux store when it's created. Fortunately, Redux Toolkit's `configureStore` function already sets that up for us automatically, so we can go ahead and use thunks here.
+Using thunks requires that the `redux-thunk` _middleware_ (a type of plugin for Redux) be added to the Redux store when it's created. Fortunately, Redux Toolkit's `configureStore` function already sets that up for us automatically, so we can go ahead and use thunks here.
+
+When writing thunks, we need to make sure the `dispatch` and `getState` methods are typed correctly. We _could_ define the thunk function as `(dispatch: AppDispatch, getState: () => RootState)`, but it's standard to define a reusable `AppThunk` type for that in the store file.
-When you need to make AJAX calls to fetch data from the server, you can put that call in a thunk. Here's an example that's written a bit longer, so you can see how it's defined:
+When you need to make HTTP calls to fetch data from the server, you can put that call in a thunk. Here's an example that's written a bit longer, so you can see how it's defined:
-```js title="features/counter/counterSlice.js"
+```ts title="Example handwritten async thunk"
// the outside "thunk creator" function
-const fetchUserById = userId => {
+const fetchUserById = (userId: string): AppThunk => {
// the inside "thunk function"
return async (dispatch, getState) => {
try {
+ dispatch(userPending())
// make an async call in the thunk
const user = await userAPI.fetchById(userId)
// dispatch an action when we get the response back
@@ -467,7 +552,55 @@ const fetchUserById = userId => {
}
```
-We'll see thunks being used in [Part 5: Async Logic and Data Fetching](./part-5-async-logic.md)
+Redux Toolkit includes a [**`createAsyncThunk`**](https://redux-toolkit.js.org/api/createAsyncThunk) method that does all of the dispatching work for you. The next function in `counterSlice.ts` is an async thunk that makes a mock API request with a counter value. When we dispatch this thunk, it will dispatch a `pending` action before making the request, and either a `fulfilled` or `rejected` action after the async logic is done.
+
+```ts title="features/counter/counterSlice.ts"
+// Thunks are commonly used for async logic like fetching data.
+// The `createAsyncThunk` method is used to generate thunks that
+// dispatch pending/fulfilled/rejected actions based on a promise.
+// In this example, we make a mock async request and return the result.
+// The `createSlice.extraReducers` field can handle these actions
+// and update the state with the results.
+export const incrementAsync = createAsyncThunk(
+ 'counter/fetchCount',
+ async (amount: number) => {
+ const response = await fetchCount(amount)
+ // The value we return becomes the `fulfilled` action payload
+ return response.data
+ }
+)
+```
+
+When you use `createAsyncThunk`, you handle its actions in `createSlice.extraReducers`. In this case, we handle all three action types, update the `status` field, and also update the `value`:
+
+```ts title="features/counter/counterSlice.ts"
+export const counterSlice = createSlice({
+ name: 'counter',
+ initialState,
+ reducers: {
+ // omit reducers
+ },
+ // The `extraReducers` field lets the slice handle actions defined elsewhere,
+ // including actions generated by createAsyncThunk or in other slices.
+ extraReducers: builder => {
+ builder
+ // Handle the action types defined by the `incrementAsync` thunk defined below.
+ // This lets the slice reducer update the state with request status and results.
+ .addCase(incrementAsync.pending, state => {
+ state.status = 'loading'
+ })
+ .addCase(incrementAsync.fulfilled, (state, action) => {
+ state.status = 'idle'
+ state.value += action.payload
+ })
+ .addCase(incrementAsync.rejected, state => {
+ state.status = 'failed'
+ })
+ }
+})
+```
+
+If you're curious _why_ we use thunks for async logic, see this deeper explanation:
@@ -510,59 +643,76 @@ This gives us a way to write whatever sync or async code we want, while still ha
-There's one more function in this file, but we'll talk about that in a minute when we look at the `` UI component.
+:::info More Info on Thunks
-:::info Want to Know More?
+We'll see thunks being used in [Part 5: Async Logic and Data Fetching](./part-5-async-logic.md)
See [the Redux Thunk docs](../../usage/writing-logic-thunks.mdx), the post [What the heck is a thunk?](https://daveceddia.com/what-is-a-thunk/) and the [Redux FAQ entry on "why do we use middleware for async?"](../../faq/Actions.md#how-can-i-represent-side-effects-such-as-ajax-calls-why-do-we-need-things-like-action-creators-thunks-and-middleware-to-do-async-behavior) for more information.
:::
-### The React Counter Component
+## The React Counter Component
Earlier, we saw what a standalone React `` component looks like. Our React+Redux app has a similar `` component, but it does a few things differently.
We'll start by looking at the `Counter.js` component file:
-```jsx title="features/counter/Counter.js"
-import React, { useState } from 'react'
-import { useSelector, useDispatch } from 'react-redux'
+```tsx title="features/counter/Counter.tsx"
+import { useState } from 'react'
+
+// Use pre-typed versions of the React-Redux
+// `useDispatch` and `useSelector` hooks
+import { useAppDispatch, useAppSelector } from '@/app/hooks'
import {
decrement,
increment,
- incrementByAmount,
incrementAsync,
- selectCount
+ incrementByAmount,
+ incrementIfOdd,
+ selectCount,
+ selectStatus
} from './counterSlice'
+
import styles from './Counter.module.css'
export function Counter() {
- const count = useSelector(selectCount)
- const dispatch = useDispatch()
+ // highlight-start
+ const dispatch = useAppDispatch()
+ const count = useAppSelector(selectCount)
+ const status = useAppSelector(selectStatus)
+ // highlight-end
const [incrementAmount, setIncrementAmount] = useState('2')
+ const incrementValue = Number(incrementAmount) || 0
+
return (
)
}
@@ -580,20 +730,11 @@ The [React-Redux library](https://react-redux.js.org/) has [a set of custom hook
First, the `useSelector` hook lets our component extract whatever pieces of data it needs from the Redux store state.
-Earlier, we saw that we can write "selector" functions, which take `state` as an argument and return some part of the state value.
-
-Our `counterSlice.js` has this selector function at the bottom:
-
-```js title="features/counter/counterSlice.js"
-// The function below is called a selector and allows us to select a value from
-// the state. Selectors can also be defined inline where they're used instead of
-// in the slice file. For example: `useSelector((state) => state.counter.value)`
-export const selectCount = state => state.counter.value
-```
+Earlier, we saw that we can write "selector" functions, which take `state` as an argument and return some part of the state value. In particular, our `counterSlice.ts` file is [exporting `selectCount` and `selectStatus`](#reading-data-with-selectors)
If we had access to a Redux store, we could retrieve the current counter value as:
-```js
+```ts
const count = selectCount(store.getState())
console.log(count)
// 0
@@ -603,14 +744,14 @@ Our components can't talk to the Redux store directly, because we're not allowed
So, we can get the current store counter value by doing:
-```js
+```ts
const count = useSelector(selectCount)
```
We don't have to _only_ use selectors that have already been exported, either. For example, we could write a selector function as an inline argument to `useSelector`:
-```js
-const countPlusTwo = useSelector(state => state.counter.value + 2)
+```ts
+const countPlusTwo = useSelector((state: RootState) => state.counter.value + 2)
```
Any time an action has been dispatched and the Redux store has been updated, `useSelector` will re-run our selector function. If the selector returns a different value than last time, `useSelector` will make sure our component re-renders with the new value.
@@ -627,16 +768,33 @@ const dispatch = useDispatch()
From there, we can dispatch actions when the user does something like clicking on a button:
-```jsx title="features/counter/Counter.js"
+```tsx title="features/counter/Counter.tsx"
```
+#### Defining Pre-Typed React-Redux Hooks
+
+By default the `useSelector` hook needs you to declare `(state: RootState)` for every selector function. We can create pre-typed versions of the `useSelector` and `useDispatch` hooks so that we don't have to keep repeating the `: RootState` part every time.
+
+```ts title="app/hooks.ts"
+import { useDispatch, useSelector } from 'react-redux'
+import type { AppDispatch, RootState } from './store'
+
+// Use throughout your app instead of plain `useDispatch` and `useSelector`
+export const useAppDispatch = useDispatch.withTypes()
+export const useAppSelector = useSelector.withTypes()
+```
+
+Then, we can import the `useAppSelector` and `useAppDispatch` hooks into our own components and use them instead of the original versions.
+
### Component State and Forms
By now you might be wondering, "Do I always have to put all my app's state into the Redux store?"
@@ -645,9 +803,11 @@ The answer is **NO. Global state that is needed across the app should go in the
In this example, we have an input textbox where the user can type in the next number to be added to the counter:
-```jsx title="features/counter/Counter.js"
+```tsx title="features/counter/Counter.tsx"
const [incrementAmount, setIncrementAmount] = useState('2')
+const incrementValue = Number(incrementAmount) || 0
+
// later
return (
@@ -659,13 +819,13 @@ return (
/>
@@ -694,33 +854,38 @@ This is also a good example of how to think about forms in Redux in general. **M
One other thing to note before we move on: remember that `incrementAsync` thunk from `counterSlice.js`? We're using it here in this component. Notice that we use it the same way we dispatch the other normal action creators. This component doesn't care whether we're dispatching a normal action or starting some async logic. It only knows that when you click that button, it dispatches something.
-### Providing the Store
+## Providing the Store
We've seen that our components can use the `useSelector` and `useDispatch` hooks to talk to the Redux store. But, since we didn't import the store, how do those hooks know what Redux store to talk to?
Now that we've seen all the different pieces of this application, it's time to circle back to the starting point of this application and see how the last pieces of the puzzle fit together.
-```jsx title="index.js"
+```tsx title="main.tsx"
import React from 'react'
-import ReactDOM from 'react-dom'
-import './index.css'
-import App from './App'
-import store from './app/store'
+import { createRoot } from 'react-dom/client'
// highlight-next-line
import { Provider } from 'react-redux'
-import * as serviceWorker from './serviceWorker'
-ReactDOM.render(
- // highlight-start
-
-
- ,
- // highlight-end
- document.getElementById('root')
+import App from './App'
+import { store } from './app/store'
+
+import './index.css'
+
+const container = document.getElementById('root')!
+const root = createRoot(container)
+
+root.render(
+
+ // highlight-start
+
+
+
+ // highlight-end
+
)
```
-We always have to call `ReactDOM.render()` to tell React to start rendering our root `` component. In order for our hooks like `useSelector` to work right, we need to use a component called `` to pass down the Redux store behind the scenes so they can access it.
+We always have to call `root.render()` to tell React to start rendering our root `` component. In order for our hooks like `useSelector` to work right, we need to use a component called `` to pass down the Redux store behind the scenes so they can access it.
We already created our store in `app/store.js`, so we can import it here. Then, we put our `` component around the whole ``, and pass in the store: ``.
@@ -743,11 +908,17 @@ Even though the counter example app is pretty small, it showed all the key piece
- Must make _immutable updates_ by copying the existing state
- Cannot contain any asynchronous logic or other "side effects"
- Redux Toolkit's `createSlice` API uses Immer to allow "mutating" immutable updates
+- **Reading values from the state is done with functions called "selectors"**
+ - Selectors accept `(state: RootState)` as their argument and either return a value from the state, or derive a new value
+ - Selectors can be written in slice files, or inline in the `useSelector` hook
- **Async logic is typically written in special functions called "thunks"**
- Thunks receive `dispatch` and `getState` as arguments
- Redux Toolkit enables the `redux-thunk` middleware by default
- **React-Redux allows React components to interact with a Redux store**
- Wrapping the app with `` enables all components to use the store
+ - The `useSelector` hook lets React components read values from the Redux store
+ - The `useDispatch` hook lets components dispatch actions
+ - For TS usage, we create pre-typed `useAppSelector` and `useAppDispatch` hooks
- Global state should go in the Redux store, local state should stay in React components
:::
diff --git a/docs/tutorials/essentials/part-3-data-flow.md b/docs/tutorials/essentials/part-3-data-flow.md
index d9115752d8..5519bcebba 100644
--- a/docs/tutorials/essentials/part-3-data-flow.md
+++ b/docs/tutorials/essentials/part-3-data-flow.md
@@ -9,6 +9,7 @@ import { DetailedExplanation } from '../../components/DetailedExplanation'
:::tip What You'll Learn
+- How to set up a Redux store in a React application
- How to add "slices" of reducer logic to the Redux store with `createSlice`
- Reading Redux data in components with the `useSelector` hook
- Dispatching actions in components with the `useDispatch` hook
@@ -18,6 +19,7 @@ import { DetailedExplanation } from '../../components/DetailedExplanation'
:::info Prerequisites
- Familiarity with key Redux terms and concepts like "actions", "reducers", "store", and "dispatching". (See [**Part 1: Redux Overview and Concepts**](./part-1-overview-concepts.md) for explanations of these terms.)
+- Basic understanding of [TypeScript syntax and usage](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html)
:::
@@ -27,9 +29,11 @@ In [Part 1: Redux Overview and Concepts](./part-1-overview-concepts.md), we look
Now that you have some idea of what these pieces are, it's time to put that knowledge into practice. We're going to build a small social media feed app, which will include a number of features that demonstrate some real-world use cases. This will help you understand how to use Redux in your own applications.
+We'll be using [TypeScript](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) syntax to write our code. You can use Redux with plain JavaScript, but using TypeScript helps prevent many common mistakes, provides built-in documentation for your code, and lets your editor show you what variable types are needed in places like React components and Redux reducers. **We strongly recommend using TypeScript for all Redux applications.**
+
:::caution
-The example app is not meant as a complete production-ready project. The goal is to help you learn the Redux APIs and typical usage patterns, and point you in the right direction using some limited examples. Also, some of the early pieces we build will be updated later on to show better ways to do things. Please read through the whole tutorial to see all the concepts in use.
+The example app is not meant as a complete production-ready project. The goal is to help you learn the Redux APIs and typical usage patterns, and point you in the right direction using some limited examples. Also, some of the early pieces we build will be updated later on to show better ways to do things. **Please read through the whole tutorial to see all the concepts in use**.
:::
@@ -41,63 +45,212 @@ To get started, you can open and fork this CodeSandbox:
-You can also [clone the same project from this Github repo](https://github.com/reduxjs/redux-essentials-example-app). After cloning the repo, you can install the tools for the project with `npm install`, and start it with `npm start`.
+You can also [clone the same project from this Github repo](https://github.com/reduxjs/redux-essentials-example-app). The project is configured to use [Yarn 4](https://yarnpkg.com/) as the package manager, but you can use any package manager ([NPM](https://docs.npmjs.com/cli/v10), [PNPM](https://pnpm.io/), or [Bun](https://bun.sh/docs/cli/install)) as you prefer. After installing packages, you can start the local dev server with the `yarn dev` command.
-If you'd like to see the final version of what we're going to build, you can check out [the **`tutorial-steps` branch**](https://github.com/reduxjs/redux-essentials-example-app/tree/tutorial-steps), or [look at the final version in this CodeSandbox](https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps).
+If you'd like to see the final version of what we're going to build, you can check out [the **`tutorial-steps-ts` branch**](https://github.com/reduxjs/redux-essentials-example-app/tree/tutorial-steps-ts), or [look at the final version in this CodeSandbox](https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/tutorial-steps-ts).
> We'd like to thank [Tania Rascia](https://www.taniarascia.com/), whose [Using Redux with React](https://www.taniarascia.com/redux-react-guide/) tutorial helped inspire the example in this page. It also uses her [Primitive UI CSS starter](https://taniarascia.github.io/primitive/) for styling.
#### Creating a New Redux + React Project
-Once you've finished this tutorial, you'll probably want to try working on your own projects. **We recommend using the [Redux template for Vite](../../introduction/Installation.md#create-a-react-redux-app) as the fastest way to create a new Redux + React project**. It comes with Redux Toolkit and React-Redux already configured, using [the same "counter" app example you saw in Part 1](./part-1-overview-concepts.md). This lets you jump right into writing your actual application code without having to add the Redux packages and set up the store.
+Once you've finished this tutorial, you'll probably want to try working on your own projects. **We recommend using the [Redux templates for Vite and Next.js](../../introduction/Installation.md#create-a-react-redux-app) as the fastest way to create a new Redux + React project**. The templates come with Redux Toolkit and React-Redux already configured, using [the same "counter" app example you saw in Part 1](./part-1-overview-concepts.md). This lets you jump right into writing your actual application code without having to add the Redux packages and set up the store.
+
+#### Exploring the Initial Project
+
+Let's take a quick look at what the initial project contains:
+
+- `/public`: base CSS styles and other static files like icons
+- `/src`
+ - `main.tsx`: the entry point file for the application, which renders the `` component. In this example, it also sets up the fake REST API on page load.
+ - `App.tsx`: the main application component. Renders the top navbar and handles client-side routing for the other content.
+ - `index.css`: styles for the complete application
+ - `/api`
+ - `client.ts`: a small `fetch` wrapper client that allows us to make HTTP GET and POST requests
+ - `server.ts`: provides a fake REST API for our data. Our app will fetch data from these fake endpoints later.
+ - `/app`
+ - `Navbar.tsx`: renders the top header and nav content
+
+If you load the app now, you should see the header and a welcome message, but no functionality.
+
+With that, let's get started!
+
+## Setting Up the Redux Store
+
+Right now the project is empty, so we'll need to start by doing the one-time setup for the Redux pieces.
+
+### Adding the Redux Packages
+
+If you look at `package.json`, you'll see that we've already installed the two packages needed to use Redux:
+
+- `@reduxjs/toolkit`: the modern Redux package, which includes all the Redux functions we'll be using to build the app
+- `react-redux`: the functions needed to let your React components talk to a Redux store
+
+If you're setting up a project from scratch, start by adding those packages to the project yourself.
+
+### Creating the Store
+
+The first step is to create an actual Redux store. **One of the principles of Redux is that there should only be _one_ store instance for an entire application**.
+
+We typically create and export the Redux store instance in its own file. The actual folder structure for the application is up to you, but it's standard to have application-wide setup and configuration in a `src/app/` folder.
-If you want to know specific details on how to add Redux to a project, see this explanation:
+We'll start by adding a `src/app/store.ts` file and creating the store.
-
+**Redux Toolkit includes a method called `configureStore`**. This function creates a new Redux store instance. It has several options that you can pass in to change the store's behavior. It also applies the most common and useful configuration settings automatically, including checking for typical mistakes, and enabling the Redux DevTools extension so that you can view the state contents and action history.
+
+```ts title="src/app/store.ts"
+import { configureStore } from '@reduxjs/toolkit'
+import type { Action } from '@reduxjs/toolkit'
+
+interface CounterState {
+ value: number
+}
+
+// An example slice reducer function that shows how a Redux reducer works inside.
+// We'll replace this soon with real app logic.
+function counterReducer(state: CounterState = { value: 0 }, action: Action) {
+ switch (action.type) {
+ // Handle actions here
+ default: {
+ return state
+ }
+ }
+}
+
+// highlight-start
+export const store = configureStore({
+ // Pass in the root reducer setup as the `reducer` argument
+ reducer: {
+ // Declare that `state.counter` will be updated by the `counterReducer` function
+ counter: counterReducer
+ }
+})
+// highlight-end
+```
+
+**`configureStore` always requires a `reducer` option**. This should typically be an object containing the individual "slice reducers" for the different parts of the application. (If necessary, you can also create the root reducer function separately and pass that as the `reducer` argument.)
+
+For this first step, we're passing in a mock slice reducer function for the `counter` slice, to show what the setup looks like. We'll replace this with a real slice reducer for the actual app we want to build in just a minute.
+
+:::tip Setup with Next.js
+
+If you're using Next.js, the setup process takes a few more steps. See the [Setup with Next.js](../../usage/nextjs.mdx) page for details on how to set up Redux with Next.js.
+
+:::
-The Redux template for Vite comes with Redux Toolkit and React-Redux already configured. If you're setting up a new project from scratch without that template, follow these steps:
+### Providing the Store
-- Add the `@reduxjs/toolkit` and `react-redux` packages
-- Create a Redux store using RTK's `configureStore` API, and pass in at least one reducer function
-- Import the Redux store into your application's entry point file (such as `src/index.js`)
-- Wrap your root React component with the `` component from React-Redux, like:
+Redux by itself is a plain JS library, and can work with any UI layer. In this app, we're using React, so we need a way to let our React components interact with the Redux store.
-```jsx
-ReactDOM.render(
-
-
- ,
- document.getElementById('root')
+To make this work, we need to use the React-Redux library and pass the Redux store into a `` component. This uses [React's Context API](https://react.dev/learn/passing-data-deeply-with-context) to make the Redux store accessible to all of the React components in our application.
+
+:::tip
+
+It's important that we _should not_ try to directly import the Redux store into other application code files! Because there's only one store file, directly importing the store can accidentally cause circular import issues (where file A imports B imports C imports A), which lead to hard-to-track bugs. Additionally, we want to be able to [write tests for the components and Redux logic](../../usage/WritingTests.mdx), and those tests will need to create their own Redux store instances. Providing the store to the components via Context keeps this flexible and avoids import problems.
+
+:::
+
+To do this, we'll import the `store` into the `main.tsx` entry point file, wrap a `` with the store around the `` component:
+
+```tsx title="src/main.tsx"
+import { createRoot } from 'react-dom/client'
+// highlight-next-line
+import { Provider } from 'react-redux'
+
+import App from './App'
+// highlight-next-line
+import { store } from './app/store'
+
+// skip mock API setup
+
+const root = createRoot(document.getElementById('root')!)
+
+root.render(
+
+ // highlight-start
+
+
+
+ // highlight-end
+
)
```
-
+### Inspecting the Redux State
-#### Exploring the Initial Project
+Now that we have a store, we can use the Redux DevTools extension to view the current Redux state.
-Let's take a quick look at what the initial project contains:
+If you open up your browser's DevTools view (such as by right-clicking anywhere in the page and choosing "Inspect"), you can click on the "Redux" tab. This will show the history of dispatched actions and the current state value:
-- `/public`: the HTML host page template and other static files like icons
-- `/src`
- - `index.js`: the entry point file for the application. It renders the React-Redux `` component and the main `` component.
- - `App.js`: the main application component. Renders the top navbar and handles client-side routing for the other content.
- - `index.css`: styles for the complete application
- - `/api`
- - `client.js`: a small AJAX request client that allows us to make GET and POST requests
- - `server.js`: provides a fake REST API for our data. Our app will fetch data from these fake endpoints later.
- - `/app`
- - `Navbar.js`: renders the top header and nav content
- - `store.js`: creates the Redux store instance
+![Redux DevTools: initial app state](/img/tutorials/essentials/devtools-initial.png)
-If you load the app now, you should see the header and a welcome message. We can also open up the Redux DevTools Extension and see that our initial Redux state is entirely empty.
+The current state value should be an object that looks like this:
-With that, let's get started!
+```ts
+{
+ counter: {
+ value: 0
+ }
+}
+```
+
+That shape was defined by the `reducer` option we passed into `configureStore`: an object, with a field named `counter`, and the slice reducer for the `counter` field returns an object like `{value}` as its state.
+
+### Exporting Store Types
+
+Since we're using TypeScript, we're going to frequently refer to TS types for "the type of the Redux state" and "the type of the Redux store `dispatch` function".
+
+We need to export those types from the `store.ts` file. We'll define the types by using the TS `typeof` operator to ask TS to infer the types based on the Redux store definition:
+
+```ts title="src/app/store.ts"
+import { configureStore } from '@reduxjs/toolkit'
+
+// omit counter slice setup
+
+export const store = configureStore({
+ reducer: {
+ counter: counterReducer
+ }
+})
+
+// highlight-start
+// Infer the type of `store`
+export type AppStore = typeof store
+// Infer the `AppDispatch` type from the store itself
+export type AppDispatch = typeof store.dispatch
+// Same for the `RootState` type
+export type RootState = ReturnType
+// highlight-end
+```
+
+If you hover over the `RootState` type in your editor, you should see `type RootState = { counter: CounterState; }`. Since this type is automatically derived from the store definition, all the future changes to the `reducer` setup will automatically be reflected in the `RootState` type as well. This way we only need to define it once, and it will always be accurate.
+
+### Exporting Typed Hooks
+
+We're going to be using React-Redux's `useSelector` and `useDispatch` hooks extensively in our components. Those need to reference the `RootState` and `AppDispatch` types each time we use the hooks.
+
+We can simplify the usage and avoid repeating the types if we set up "pre-typed" versions of those hooks that have the right types already built in.
+
+React-Redux 9.1 includes `.withTypes()` methods that apply the right types to those hooks. We can export these pre-typed hooks, then use them in the rest of the application:
+
+```ts title="src/app/hooks.ts"
+// This file serves as a central hub for re-exporting pre-typed Redux hooks.
+import { useDispatch, useSelector } from 'react-redux'
+import type { AppDispatch, RootState } from './store'
+
+// highlight-start
+// Use throughout your app instead of plain `useDispatch` and `useSelector`
+export const useAppDispatch = useDispatch.withTypes()
+export const useAppSelector = useSelector.withTypes()
+// highlight-end
+```
+
+That completes the setup process. Let's start building the app!
## Main Posts Feed
@@ -105,9 +258,13 @@ The main feature for our social media feed app will be a list of posts. We'll ad
### Creating the Posts Slice
-The first step is to create a new Redux "slice" that will contain the data for our posts. Once we have that data in the Redux store, we can create the React components to show that data on the page.
+The first step is to create a new Redux "slice" that will contain the data for our posts.
+
+**A "slice" is a collection of Redux reducer logic and actions for a single feature in your app**, typically defined together in a single file. The name comes from splitting up the root Redux state object into multiple "slices" of state.
-Inside of `src`, create a new `features` folder, put a `posts` folder inside of `features`, and add a new file named `postsSlice.js`.
+Once we have the posts data in the Redux store, we can create the React components to show that data on the page.
+
+Inside of `src`, create a new `features` folder, put a `posts` folder inside of `features`, and add a new file named `postsSlice.ts`.
We're going to use the Redux Toolkit `createSlice` function to make a reducer function that knows how to handle our posts data. Reducer functions need to have some initial data included so that the Redux store has those values loaded when the app starts up.
@@ -115,32 +272,47 @@ For now, we'll create an array with some fake post objects inside so that we can
We'll import `createSlice`, define our initial posts array, pass that to `createSlice`, and export the posts reducer function that `createSlice` generated for us:
-```js title="features/posts/postsSlice.js"
+```ts title="features/posts/postsSlice.ts"
import { createSlice } from '@reduxjs/toolkit'
-const initialState = [
+// Define a TS type for the data we'll be using
+export interface Post {
+ id: string
+ title: string
+ content: string
+}
+
+// Create an initial state value for the reducer, with that type
+const initialState: Post[] = [
{ id: '1', title: 'First Post!', content: 'Hello!' },
{ id: '2', title: 'Second Post', content: 'More text' }
]
+// Create the slice and pass in the initial state
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {}
})
+// Export the generated reducer function
export default postsSlice.reducer
```
-Every time we create a new slice, we need to add its reducer function to our Redux store. We already have a Redux store being created, but right now it doesn't have any data inside. Open up `app/store.js`, import the `postsReducer` function, and update the call to `configureStore` so that the `postsReducer` is being passed as a reducer field named `posts`:
+Every time we create a new slice, we need to add its reducer function to our Redux store. We already have a Redux store being created, but right now it doesn't have any data inside. Open up `app/store.ts`, import the `postsReducer` function, remove all of the `counter` code, and update the call to `configureStore` so that the `postsReducer` is being passed as a reducer field named `posts`:
-```js title="app/store.js"
+```ts title="app/store.ts"
import { configureStore } from '@reduxjs/toolkit'
-import postsReducer from '../features/posts/postsSlice'
+// highlight-next-line
+// Removed the `counterReducer` function, `CounterState` type, and `Action` import
-export default configureStore({
+// highlight-next-line
+import postsReducer from '@/features/posts/postsSlice'
+
+export const store = configureStore({
reducer: {
+ // highlight-next-line
posts: postsReducer
}
})
@@ -154,18 +326,23 @@ We can confirm that this works by opening the Redux DevTools Extension and looki
### Showing the Posts List
-Now that we have some posts data in our store, we can create a React component that shows the list of posts. All of the code related to our feed posts feature should go in the `posts` folder, so go ahead and create a new file named `PostsList.js` in there.
+Now that we have some posts data in our store, we can create a React component that shows the list of posts. All of the code related to our feed posts feature should go in the `posts` folder, so go ahead and create a new file named `PostsList.tsx` in there. (Note that since this is a React component written in TypeScript and using JSX syntax, it needs a `.tsx` file extension for TypeScript to compile it properly)
If we're going to render a list of posts, we need to get the data from somewhere. React components can read data from the Redux store using the `useSelector` hook from the React-Redux library. The "selector functions" that you write will be called with the entire Redux `state` object as a parameter, and should return the specific data that this component needs from the store.
+Since we're using TypeScript, all of our components should always use the pre-typed `useAppSelector` hook that we added in `src/app/hooks.ts`, since that has the right `RootState` type already included.
+
Our initial `PostsList` component will read the `state.posts` value from the Redux store, then loop over the array of posts and show each of them on screen:
-```jsx title="features/posts/PostsList.js"
-import React from 'react'
-import { useSelector } from 'react-redux'
+```tsx title="features/posts/PostsList.tsx"
+// highlight-next-line
+import { useAppSelector } from '@/app/hooks'
export const PostsList = () => {
- const posts = useSelector(state => state.posts)
+ // highlight-start
+ // Select the `state.posts` value from the store into the component
+ const posts = useAppSelector(state => state.posts)
+ // highlight-end
const renderedPosts = posts.map(post => (
@@ -183,19 +360,12 @@ export const PostsList = () => {
}
```
-We then need to update the routing in `App.js` so that we show the `PostsList` component instead of the "welcome" message. Import the `PostsList` component into `App.js`, and replace the welcome text with ``. We'll also wrap it in a [React Fragment](https://react.dev/reference/react/Fragment), because we're going to add something else to the main page soon:
-
-```jsx title="App.js"
-import React from 'react'
-import {
- BrowserRouter as Router,
- Switch,
- Route,
- Redirect
-} from 'react-router-dom'
+We then need to update the routing in `App.tsx` so that we show the `PostsList` component instead of the "welcome" message. Import the `PostsList` component into `App.tsx`, and replace the welcome text with ``. We'll also wrap it in a [React Fragment](https://react.dev/reference/react/Fragment), because we're going to add something else to the main page soon:
-import { Navbar } from './app/Navbar'
+```tsx title="App.tsx"
+import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
+import { Navbar } from './components/Navbar'
// highlight-next-line
import { PostsList } from './features/posts/PostsList'
@@ -204,20 +374,18 @@ function App() {
)
@@ -240,58 +408,73 @@ We'll create the empty form first and add it to the page. Then, we'll connect th
#### Adding the New Post Form
-Create `AddPostForm.js` in our `posts` folder. We'll add a text input for the post title, and a text area for the body of the post:
+Create `AddPostForm.tsx` in our `posts` folder. We'll add a text input for the post title, and a text area for the body of the post:
+
+```tsx title="features/posts/AddPostForm.tsx"
+import React from 'react'
-```jsx title="features/posts/AddPostForm.js"
-import React, { useState } from 'react'
+// TS types for the input fields
+// See: https://epicreact.dev/how-to-type-a-react-form-on-submit-handler/
+interface AddPostFormFields extends HTMLFormControlsCollection {
+ postTitle: HTMLInputElement
+ postContent: HTMLTextAreaElement
+}
+interface AddPostFormElements extends HTMLFormElement {
+ readonly elements: AddPostFormFields
+}
export const AddPostForm = () => {
- const [title, setTitle] = useState('')
- const [content, setContent] = useState('')
+ const handleSubmit = (e: React.FormEvent) => {
+ // Prevent server submission
+ e.preventDefault()
+
+ const { elements } = e.currentTarget
+ const title = elements.postTitle.value
+ const content = elements.postContent.value
- const onTitleChanged = e => setTitle(e.target.value)
- const onContentChanged = e => setContent(e.target.value)
+ console.log('Values: ', { title, content })
+
+ e.currentTarget.reset()
+ }
return (
Add a New Post
-
)
}
```
-Import that component into `App.js`, and add it right above the `` component:
+Note that this doesn't have any Redux-specific logic yet - we'll add that next.
+
+In this example we're using ["uncontrolled" inputs](https://react.dev/reference/react-dom/components/input#reading-the-input-values-when-submitting-a-form) and using HTML5 form validation to prevent submitting empty input fields, but it's up to you how you read values from a form - that's a preference about React usage patterns and not specific to Redux.
+
+Import that component into `App.tsx`, and add it right above the `` component:
-```jsx title="App.js"
+```tsx title="App.tsx"
+// omit outer `` definition
(
-
+ element={
+ <>
// highlight-next-line
-
- )}
-/>
+ >
+ }
+>
```
You should see the form show up in the page right below the header.
@@ -304,38 +487,65 @@ Our posts slice is responsible for handling all updates to the posts data. Insid
Inside of `reducers`, add a function named `postAdded`, which will receive two arguments: the current `state` value, and the `action` object that was dispatched. Since the posts slice _only_ knows about the data it's responsible for, the `state` argument will be the array of posts by itself, and not the entire Redux state object.
-The `action` object will have our new post entry as the `action.payload` field, and we'll put that new post object into the `state` array.
+The `action` object will have our new post entry as the `action.payload` field. When we declare the reducer function, we also need to tell TypeScript what that actual `action.payload` type is, so that it can correctly check when we pass in the argument and access the `action.payload` contents. To do that, we need to import the `PayloadAction` type from Redux Toolkit, and declare the `action` argument as `action: PayloadAction`. In this case, that will be `action: PayloadAction`.
+
+The actual state update is adding the new post object into the `state` array, which we can do via `state.push()` in the reducer.
+
+:::warning
+
+Remember: **Redux reducer functions must _always_ create new state values immutably, by making copies!** It's safe to call mutating functions like `Array.push()` or modify object fields like `state.someField = someValue` inside of `createSlice()`, because [it converts those mutations into safe immutable updates internally using the Immer library](./part-2-app-structure.md#reducers-and-immutable-updates), but **don't try to mutate any data outside of `createSlice`!**
+
+:::
+
+When we write the `postAdded` reducer function, `createSlice` will automatically generate an ["action creator" function](../fundamentals/part-7-standard-patterns.md#action-creators) with the same name. We can export that action creator and use it in our UI components to dispatch the action when the user clicks "Save Post".
+
+```ts title="features/posts/postsSlice.ts"
+// highlight-start
+// Import the `PayloadAction` TS type
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+// highlight-end
-When we write the `postAdded` reducer function, `createSlice` will automatically generate an "action creator" function with the same name. We can export that action creator and use it in our UI components to dispatch the action when the user clicks "Save Post".
+// omit initial state
-```js title="features/posts/postsSlice.js"
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// highlight-start
- postAdded(state, action) {
+ // Declare a "case reducer" named `postAdded`.
+ // The type of `action.payload` will be a `Post` object.
+ postAdded(state, action: PayloadAction) {
+ // "Mutate" the existing state array, which is
+ // safe to do here because `createSlice` uses Immer inside.
state.push(action.payload)
}
// highlight-end
}
})
-// highlight-next-line
+// highlight-start
+// Export the auto-generated action creator with the same name
export const { postAdded } = postsSlice.actions
+// highlight-end
export default postsSlice.reducer
```
-:::warning
-
-Remember: **reducer functions must _always_ create new state values immutably, by making copies!** It's safe to call mutating functions like `Array.push()` or modify object fields like `state.someField = someValue` inside of `createSlice()`, because it converts those mutations into safe immutable updates internally using the Immer library, but **don't try to mutate any data outside of `createSlice`!**
+Terminology-wise, `postAdded` here is an example of a **"case reducer"**. It's a reducer function, inside of a slice, that handles one specific action type that was dispatched. Conceptually, it's like we wrote a `case` statement inside of a `switch` - "when we see this exact action type, run this logic":
-:::
+```ts
+function sliceReducer(state = initialState, action) {
+ switch (action.type) {
+ case 'posts/postAdded': {
+ // update logic here
+ }
+ }
+}
+```
#### Dispatching the "Post Added" Action
-Our `AddPostForm` has text inputs and a "Save Post" button, but the button doesn't do anything yet. We need to add a click handler that will dispatch the `postAdded` action creator and pass in a new post object containing the title and content the user wrote.
+Our `AddPostForm` has text inputs and a "Save Post" button that triggers a submit handler, but the button doesn't do anything yet. We need to update the submit handler to dispatch the `postAdded` action creator and pass in a new post object containing the title and content the user wrote.
Our post objects also need to have an `id` field. Right now, our initial test posts are using some fake numbers for their IDs. We could write some code that would figure out what the next incrementing ID number should be, but it would be better if we generated a random unique ID instead. Redux Toolkit has a `nanoid` function we can use for that.
@@ -345,55 +555,64 @@ We'll talk more about generating IDs and dispatching actions in [Part 4: Using R
:::
-In order to dispatch actions from a component, we need access to the store's `dispatch` function. We get this by calling the `useDispatch` hook from React-Redux. We also need to import the `postAdded` action creator into this file.
+In order to dispatch actions from a component, **we need access to the store's `dispatch` function**. We get this by calling the `useDispatch` hook from React-Redux. Since we're using TypeScript, that means that we should actually **import the `useAppDispatch` hook with the right types**. We also need to import the `postAdded` action creator into this file.
-Once we have the `dispatch` function available in our component, we can call `dispatch(postAdded())` in a click handler. We can take the title and content values from our React component `useState` hooks, generate a new ID, and put them together into a new post object that we pass to `postAdded()`.
+Once we have the `dispatch` function available in our component, we can call `dispatch(postAdded())` in a click handler. We can take the title and content values from our form, generate a new ID, and put them together into a new post object that we pass to `postAdded()`.
-```jsx title="features/posts/AddPostForm"
-import React, { useState } from 'react'
+```tsx title="features/posts/AddPostForm.tsx"
+import React from 'react'
// highlight-start
-import { useDispatch } from 'react-redux'
import { nanoid } from '@reduxjs/toolkit'
-import { postAdded } from './postsSlice'
+import { useAppDispatch } from '@/app/hooks'
+
+import { type Post, postAdded } from './postsSlice'
// highlight-end
+// omit form types
+
export const AddPostForm = () => {
- const [title, setTitle] = useState('')
- const [content, setContent] = useState('')
+ // highlight-start
+ // Get the `dispatch` method from the store
+ const dispatch = useAppDispatch()
+
+ // highlight-end
- // highlight-next-line
- const dispatch = useDispatch()
+ const handleSubmit = (e: React.FormEvent) => {
+ // Prevent server submission
+ e.preventDefault()
- const onTitleChanged = e => setTitle(e.target.value)
- const onContentChanged = e => setContent(e.target.value)
+ const { elements } = e.currentTarget
+ const title = elements.postTitle.value
+ const content = elements.postContent.value
- // highlight-start
- const onSavePostClicked = () => {
- if (title && content) {
- dispatch(
- postAdded({
- id: nanoid(),
- title,
- content
- })
- )
-
- setTitle('')
- setContent('')
+ // highlight-start
+ // Create the post object and dispatch the `postAdded` action
+ const newPost: Post = {
+ id: nanoid(),
+ title,
+ content
}
+ dispatch(postAdded(newPost))
+ // highlight-end
+
+ e.currentTarget.reset()
}
- // highlight-end
return (
Add a New Post
-
)
@@ -420,14 +639,25 @@ We can check the Redux DevTools Extension to see the action we dispatched, and l
The "Diff" tab should also show us that `state.posts` had one new item added, which is at index 2.
-Notice that our `AddPostForm` component has some React `useState` hooks inside, to keep track of the title and content values the user is typing in. Remember, **the Redux store should only contain data that's considered "global" for the application!** In this case, only the `AddPostForm` will need to know about the latest values for the input fields, so we want to keep that data in React component state instead of trying to keep the temporary data in the Redux store. When the user is done with the form, we dispatch a Redux action to update the store with the final values based on the user input.
+Remember, **the Redux store should only contain data that's considered "global" for the application!** In this case, only the `AddPostForm` will need to know about the latest values for the input fields. Even if we built the form with ["controlled" inputs](https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable), we'd want to keep the data in React component state instead of trying to keep the temporary data in the Redux store. When the user is done with the form, we dispatch a Redux action to update the store with the final values based on the user input.
## What You've Learned
+We've set up the basics of a Redux app - store, slice with reducers, and UI to dispatch actions. Here's what the app looks like so far:
+
+
+
Let's recap what you've learned in this section:
:::tip Summary
+- **A Redux app has a single `store` that is passed to React components via a `` component**
- **Redux state is updated by "reducer functions"**:
- Reducers always calculate a new state _immutably_, by copying existing state values and modifying the copies with the new data
- The Redux Toolkit `createSlice` function generates "slice reducer" functions for you, and lets you write "mutating" code that is turned into safe immutable updates
@@ -439,20 +669,11 @@ Let's recap what you've learned in this section:
- `createSlice` will generate action creator functions for each reducer we add to a slice
- Call `dispatch(someActionCreator())` in a component to dispatch an action
- Reducers will run, check to see if this action is relevant, and return new state if appropriate
- - Temporary data like form input values should be kept as React component state. Dispatch a Redux action to update the store when the user is done with the form.
+ - Temporary data like form input values should be kept as React component state or plain HTML input fields. Dispatch a Redux action to update the store when the user is done with the form.
+- **If you're using TypeScript, the initial app setup should define TS types for `RootState` and `AppDispatch` based on the store, and export pre-typed versions of the React-Redux `useSelector` and `useDispatch` hooks**
:::
-Here's what the app looks like so far:
-
-
-
## What's Next?
Now that you know the basic Redux data flow, move on to [Part 4: Using Redux Data](./part-4-using-data.md), where we'll add some additional functionality to our app and see examples of how to work with the data that's already in the store.
diff --git a/docs/tutorials/essentials/part-4-using-data.md b/docs/tutorials/essentials/part-4-using-data.md
index bd35dcf51e..af7ab4ef87 100644
--- a/docs/tutorials/essentials/part-4-using-data.md
+++ b/docs/tutorials/essentials/part-4-using-data.md
@@ -11,7 +11,9 @@ import { DetailedExplanation } from '../../components/DetailedExplanation'
- Using Redux data in multiple React components
- Organizing logic that dispatches actions
+- Using selectors to look up state values
- Writing more complex update logic in reducers
+- How to think about Redux actions
:::
@@ -24,9 +26,9 @@ import { DetailedExplanation } from '../../components/DetailedExplanation'
## Introduction
-In [Part 3: Basic Redux Data Flow](./part-3-data-flow.md), we saw how to start from an empty Redux+React project setup, add a new slice of state, and create React components that can read data from the Redux store and dispatch actions to update that data. We also looked at how data flows through the application, with components dispatching actions, reducers processing actions and returning new state, and components reading the new state and rerendering the UI.
+In [Part 3: Basic Redux Data Flow](./part-3-data-flow.md), we saw how to start from an empty Redux+React project setup, add a new slice of state, and create React components that can read data from the Redux store and dispatch actions to update that data. We also looked at how data flows through the application, with components dispatching actions, reducers processing actions and returning new state, and components reading the new state and rerendering the UI. We also saw how to create "pre-typed" versions of the `useSelector` and `useDispatch` hooks that have the correct store types applied automatically.
-Now that you know the core steps to write Redux logic, we're going to use those same steps to add some new features to our social media feed that will make it more useful: viewing a single post, editing existing posts, showing post author details, post timestamps, and reaction buttons.
+Now that you know the core steps to write Redux logic, we're going to use those same steps to add some new features to our social media feed that will make it more useful: viewing a single post, editing existing posts, showing post author details, post timestamps, reaction buttons, and auth.
:::info
@@ -44,14 +46,15 @@ Currently, our post entries are being shown in the main feed page, but if the te
First, we need to add a new `SinglePostPage` component to our `posts` feature folder. We'll use React Router to show this component when the page URL looks like `/posts/123`, where the `123` part should be the ID of the post we want to show.
-```jsx title="features/posts/SinglePostPage.js"
-import React from 'react'
-import { useSelector } from 'react-redux'
+```tsx title="features/posts/SinglePostPage.tsx"
+import { useParams } from 'react-router-dom'
+
+import { useAppSelector } from '@/app/hooks'
-export const SinglePostPage = ({ match }) => {
- const { postId } = match.params
+export const SinglePostPage = () => {
+ const { postId } = useParams()
- const post = useSelector(state =>
+ const post = useAppSelector(state =>
state.posts.find(post => post.id === postId)
)
@@ -74,15 +77,15 @@ export const SinglePostPage = ({ match }) => {
}
```
-React Router will pass in a `match` object as a prop that contains the URL information we're looking for. When we set up the route to render this component, we're going to tell it to parse the second part of the URL as a variable named `postId`, and we can read that value from `match.params`.
+When we set up the route to render this component, we're going to tell it to parse the second part of the URL as a variable named `postId`, and we can read that value from the `useParams` hook.
Once we have that `postId` value, we can use it inside a selector function to find the right post object from the Redux store. We know that `state.posts` should be an array of all post objects, so we can use the `Array.find()` function to loop through the array and return the post entry with the ID we're looking for.
-It's important to note that **the component will re-render any time the value returned from `useSelector` changes to a new reference**. Components should always try to select the smallest possible amount of data they need from the store, which will help ensure that it only renders when it actually needs to.
+It's important to note that **the component will re-render any time the value returned from `useAppSelector` changes to a new reference**. Components should always try to select the smallest possible amount of data they need from the store, which will help ensure that it only renders when it actually needs to.
It's possible that we might not have a matching post entry in the store - maybe the user tried to type in the URL directly, or we don't have the right data loaded. If that happens, the `find()` function will return `undefined` instead of an actual post object. Our component needs to check for that and handle it by showing a "Post not found!" message in the page.
-Assuming we do have the right post object in the store, `useSelector` will return that, and we can use it to render the title and content of the post in the page.
+Assuming we do have the right post object in the store, `useAppSelector` will return that, and we can use it to render the title and content of the post in the page.
You might notice that this looks fairly similar to the logic we have in the body of our `` component, where we loop over the whole `posts` array to show post excerpts on the main feed. We _could_ try to extract a `Post` component that could be used in both places, but there are already some differences in how we're showing a post excerpt and the whole post. It's usually better to keep writing things separately for a while even if there's some duplication, and then we can decide later if the different sections of code are similar enough that we can really extract a reusable component.
@@ -90,60 +93,55 @@ You might notice that this looks fairly similar to the logic we have in the body
Now that we have a `` component, we can define a route to show it, and add links to each post in the front page feed.
-We'll import `SinglePostPage` in `App.js`, and add the route:
+While we're at it, it's also worth extracting the "main page" content into a separate `` component as well, just for readability.
-```jsx title="App.js"
-import { PostsList } from './features/posts/PostsList'
-import { AddPostForm } from './features/posts/AddPostForm'
-// highlight-next-line
+We'll import `PostsMainPage` and `SinglePostPage` in `App.tsx`, and add the route:
+
+```tsx title="App.tsx"
+import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
+
+import { Navbar } from './components/Navbar'
+// highlight-start
+import { PostsMainPage } from './features/posts/PostsMainPage'
import { SinglePostPage } from './features/posts/SinglePostPage'
+// highlight-end
function App() {
return (
)
}
+
+export default App
```
Then, in ``, we'll update the list rendering logic to include a `` that routes to that specific post:
-```jsx title="features/posts/PostsList.js"
-import React from 'react'
-import { useSelector } from 'react-redux'
+```tsx title="features/posts/PostsList.tsx"
// highlight-next-line
import { Link } from 'react-router-dom'
+import { useAppSelector } from '@/app/hooks'
export const PostsList = () => {
- const posts = useSelector(state => state.posts)
+ const posts = useAppSelector(state => state.posts)
const renderedPosts = posts.map(post => (
-
{post.title}
+
+ // highlight-next-line
+ {post.title}
+
{post.content.substring(0, 100)}
- // highlight-start
-
- View Post
-
- // highlight-end
))
@@ -158,9 +156,7 @@ export const PostsList = () => {
And since we can now click through to a different page, it would also be helpful to have a link back to the main posts page in the `` component as well:
-```jsx title="app/Navbar.js"
-import React from 'react'
-
+```tsx title="app/Navbar.tsx"
// highlight-next-line
import { Link } from 'react-router-dom'
@@ -171,11 +167,10 @@ export const Navbar = () => {
Redux Essentials Example
- // highlight-start
+ // highlight-next-line
Posts
- // highlight-end
@@ -195,6 +190,14 @@ First, we need to update our `postsSlice` to create a new reducer function and a
Inside of the `createSlice()` call, we should add a new function into the `reducers` object. Remember that the name of this reducer should be a good description of what's happening, because we're going to see the reducer name show up as part of the action type string in the Redux DevTools whenever this action is dispatched. Our first reducer was called `postAdded`, so let's call this one `postUpdated`.
+:::tip
+
+Redux itself doesn't care what name you use for these reducer functions - it'll run the same if it's named `postAdded`, `addPost`, `POST_ADDED`, or `someRandomName`.
+
+That said, **we encourage naming reducers as past-tense "this happened" names like `postAdded`, because we're describing "an event that occurred in the application"**.
+
+:::
+
In order to update a post object, we need to know:
- The ID of the post being updated, so that we can find the right post object in the state
@@ -202,7 +205,7 @@ In order to update a post object, we need to know:
Redux action objects are required to have a `type` field, which is normally a descriptive string, and may also contain other fields with more information about what happened. By convention, we normally put the additional info in a field called `action.payload`, but it's up to us to decide what the `payload` field contains - it could be a string, a number, an object, an array, or something else. In this case, since we have three pieces of information we need, let's plan on having the `payload` field be an object with the three fields inside of it. That means the action object will look like `{type: 'posts/postUpdated', payload: {id, title, content}}`.
-By default, the action creators generated by `createSlice` expect you to pass in one argument, and that value will be put into the action object as `action.payload`. So, we can pass an object containing those fields as the argument to the `postUpdated` action creator.
+By default, the action creators generated by `createSlice` expect you to pass in one argument, and that value will be put into the action object as `action.payload`. So, we can pass an object containing those fields as the argument to the `postUpdated` action creator. As with `postAdded`, this is an entire `Post` object, so we declare that the reducer argument is `action: PayloadAction`.
We also know that the reducer is responsible for determining how the state should actually be updated when an action is dispatched. Given that, we should have the reducer find the right post object based on the ID, and specifically update the `title` and `content` fields in that post.
@@ -210,16 +213,21 @@ Finally, we'll need to export the action creator function that `createSlice` gen
Given all those requirements, here's how our `postsSlice` definition should look after we're done:
-```js title="features/posts/postsSlice.js"
+```ts title="features/posts/postsSlice.ts"
+// highlight-next-line
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+
+// omit state types
+
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
- postAdded(state, action) {
+ postAdded(state, action: PayloadAction) {
state.push(action.payload)
},
// highlight-start
- postUpdated(state, action) {
+ postUpdated(state, action: PayloadAction) {
const { id, title, content } = action.payload
const existingPost = state.find(post => post.id === id)
if (existingPost) {
@@ -239,72 +247,85 @@ export default postsSlice.reducer
### Creating an Edit Post Form
-Our new `` component will look similar to the ``, but the logic needs to be a bit different. We need to retrieve the right `post` object from the store, then use that to initialize the state fields in the component so the user can make changes. We'll save the changed title and content values back to the store after the user is done. We'll also use React Router's history API to switch over to the single post page and show that post.
+Our new `` component will look similar to both the the `` and ``, but the logic needs to be a bit different. We need to retrieve the right `post` object from the store based on the `postId` in the URL, then use that to initialize the input fields in the component so the user can make changes. We'll save the changed title and content values back to the store when the user submits the form. We'll also use React Router's `useNavigate` hook to switch over to the single post page and show that post after they save the changes.
-```jsx title="features/posts/EditPostForm.js"
-import React, { useState } from 'react'
-import { useDispatch, useSelector } from 'react-redux'
-import { useHistory } from 'react-router-dom'
+```tsx title="features/posts/EditPostForm.tsx"
+import React from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
+import { useAppSelector, useAppDispatch } from '@/app/hooks'
import { postUpdated } from './postsSlice'
-export const EditPostForm = ({ match }) => {
- const { postId } = match.params
+// omit form element types
+
+export const EditPostForm = () => {
+ const { postId } = useParams()
- const post = useSelector(state =>
+ const post = useAppSelector(state =>
state.posts.find(post => post.id === postId)
)
- const [title, setTitle] = useState(post.title)
- const [content, setContent] = useState(post.content)
+ const dispatch = useAppDispatch()
+ const navigate = useNavigate()
+
+ if (!post) {
+ return (
+
+
-
-
)
}
```
-Like with `SinglePostPage`, we'll need to import it into `App.js` and add a route that will render this component with the `postId` as a route parameter.
+Note that the Redux-specific code here is relatively minimal. Once again, we read a value from the Redux store via `useAppSelector`, and then dispatch an action via `useAppDispatch` when the user interacts with the UI.
-```jsx title="App.js"
-import { PostsList } from './features/posts/PostsList'
-import { AddPostForm } from './features/posts/AddPostForm'
+Like with `SinglePostPage`, we'll need to import it into `App.tsx` and add a route that will render this component with the `postId` as a route parameter.
+
+```tsx title="App.tsx"
+import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
+
+import { Navbar } from './components/Navbar'
+import { PostsMainPage } from './features/posts/PostsMainPage'
import { SinglePostPage } from './features/posts/SinglePostPage'
// highlight-next-line
import { EditPostForm } from './features/posts/EditPostForm'
@@ -314,39 +335,31 @@ function App() {
)
}
+
+export default App
```
We should also add a new link to our `SinglePostPage` that will route to `EditPostForm`, like:
-```jsx title="features/post/SinglePostPage.js"
+```tsx title="features/post/SinglePostPage.tsx"
// highlight-next-line
-import { Link } from 'react-router-dom'
+import { Link, useParams } from 'react-router-dom'
-export const SinglePostPage = ({ match }) => {
+export const SinglePostPage = () => {
// omit other contents
-
{post.content}
+
{post.content}
// highlight-start
Edit Post
@@ -368,9 +381,9 @@ If an action needs to contain a unique ID or some other random value, always gen
If we were writing the `postAdded` action creator by hand, we could have put the setup logic inside of it ourselves:
-```js
+```ts
// hand-written action creator
-function postAdded(title, content) {
+function postAdded(title: string, content: string) {
const id = nanoid()
return {
type: 'posts/postAdded',
@@ -385,23 +398,19 @@ Fortunately, `createSlice` lets us define a "prepare callback" function when we
Inside of the `reducers` field in `createSlice`, we can define one of the fields as an object that looks like `{reducer, prepare}`:
-```js title="features/posts/postsSlice.js"
+```ts title="features/posts/postsSlice.ts"
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// highlight-start
postAdded: {
- reducer(state, action) {
+ reducer(state, action: PayloadAction) {
state.push(action.payload)
},
- prepare(title, content) {
+ prepare(title: string, content: string) {
return {
- payload: {
- id: nanoid(),
- title,
- content
- }
+ payload: { id: nanoid(), title, content }
}
}
}
@@ -413,31 +422,191 @@ const postsSlice = createSlice({
Now our component doesn't have to worry about what the payload object looks like - the action creator will take care of putting it together the right way. So, we can update the component so that it passes in `title` and `content` as arguments when it dispatches `postAdded`:
-```jsx title="features/posts/AddPostForm.js"
-const onSavePostClicked = () => {
- if (title && content) {
- // highlight-next-line
- dispatch(postAdded(title, content))
- setTitle('')
- setContent('')
- }
+```ts title="features/posts/AddPostForm.tsx"
+const handleSubmit = (e: React.FormEvent) => {
+ // Prevent server submission
+ e.preventDefault()
+
+ const { elements } = e.currentTarget
+ const title = elements.postTitle.value
+ const content = elements.postContent.value
+
+ // highlight-start
+ // Now we can pass these in as separate arguments,
+ // and the ID will be generated automatically
+ dispatch(postAdded(title, content))
+ // highlight-end
+
+ e.currentTarget.reset()
+}
+```
+
+## Reading Data With Selectors
+
+We now have a couple different components that are looking up a post by ID, and repeating the `state.posts.find()` call. This is duplicate code, and it's always worth _considering_ if we should de-duplicate things. It's also fragile - as we'll see in later sections, we are eventually going to start changing the posts slice state structure. When we do that, we'll have to find each place that we reference `state.posts` and update the logic accordingly. TypeScript will help catch broken code that no longer matches the expected state type by throwing errors at compile time, but it would be nice if we didn't have to keep rewriting our components every time we made a change to the data format in our reducers, and didn't have to repeat logic in the components.
+
+One way to avoid this is to **define reusable selector functions in the slice files**, and have the components use those selectors to extract the data they need instead of repeating the selector logic in each component. That way, if we do change our state structure again, we only need to update the code in the slice file.
+
+### Defining Selector Functions
+
+You've already been writing selector functions every time we called `useAppSelector`, such as `useAppSelector( state => state.posts )`. In that case, the selector is being defined inline. Since it's just a function, we could also write it as:
+
+```ts
+const selectPosts = (state: RootState) => state.posts
+const posts = useAppSelector(selectPosts)
+```
+
+Selectors are typically written as standalone individual functions in a slice file. They normally accept the entire Redux `RootState` as the first argument, and may also accept other arguments as well.
+
+### Extracting Posts Selectors
+
+The `` component needs to read a list of all the posts, and the `` and `` components need to look up a single post by its ID. Let's export two small selector functions from `postsSlice.ts` to cover those cases:
+
+```ts title="features/posts/postsSlice.ts"
+import type { RootState } from '@/app/store'
+
+const postsSlice = createSlice(/* omit slice code*/)
+
+export const { postAdded, postUpdated, reactionAdded } = postsSlice.actions
+
+export default postsSlice.reducer
+
+// highlight-start
+export const selectAllPosts = (state: RootState) => state.posts
+
+export const selectPostById = (state: RootState, postId: string) =>
+ state.posts.find(post => post.id === postId)
+//highlight-end
+```
+
+Note that the `state` parameter for these selector functions is the root Redux state object, as it was for the inlined anonymous selectors we wrote directly inside of `useAppSelector`.
+
+We can then use them in the components:
+
+```tsx title="features/posts/PostsList.tsx"
+// omit imports
+// highlight-next-line
+import { selectAllPosts } from './postsSlice'
+
+export const PostsList = () => {
+ // highlight-next-line
+ const posts = useAppSelector(selectAllPosts)
+ // omit component contents
+}
+```
+
+```tsx title="features/posts/SinglePostPage.tsx"
+// omit imports
+//highlight-next-line
+import { selectPostById } from './postsSlice'
+
+export const SinglePostPage = () => {
+ const { postId } = useParams()
+
+ // highlight-next-line
+ const post = useAppSelector(state => selectPostById(state, postId!))
+ // omit component logic
+}
+```
+
+```ts title="features/posts/EditPostForm.tsx"
+// omit imports
+//highlight-next-line
+import { postUpdated, selectPostById } from './postsSlice'
+
+export const EditPostForm = () => {
+ const { postId } = useParams()
+
+ // highlight-next-line
+ const post = useAppSelector(state => selectPostById(state, postId!))
+ // omit component logic
}
```
+Note that the `postId` we get from `useParams()` is typed as `string | undefined`, but `selectPostById` expects a valid `string` as the argument. We can use the TS `!` operator to tell the TS compiler this value will not be `undefined` at this point in the code. (This can be dangerous, but we can make the assumption because we know the routing setup only shows `` if there's a post ID in the URL.)
+
+We'll continue this pattern of writing selectors in slices as we go forward, rather than writing them inline inside of `useAppSelector` in components. Remember, this isn't required, but it's a good pattern to follow!
+
+### Using Selectors Effectively
+
+It's often a good idea to encapsulate data lookups by writing reusable selectors. Ideally, components don't even have to know where in the Redux `state` a value lives - they just use a selector from the slice to access the data.
+
+You can also create "memoized" selectors that can help improve performance by optimizing rerenders and skipping unnecessary recalculations, which we'll look at in a later part of this tutorial.
+
+But, like any abstraction, it's not something you should do _all_ the time, everywhere. Writing selectors means more code to understand and maintain. **Don't feel like you need to write selectors for every single field of your state**. Try starting without any selectors, and add some later when you find yourself looking up the same values in many parts of your application code.
+
+### Optional: Defining Selectors Inside of `createSlice`
+
+We've seen that we can write selectors as standalone functions in slice files. In some cases, you can shorten this a bit by defining selectors directly inside `createSlice` itself.
+
+
+
+We've already seen that `createSlice` requires the `name`, `initialState`, and `reducers` fields, and also accepts an optional `extraReducers` field.
+
+If you want to define selectors directly inside of `createSlice`, you can pass in an additional `selectors` field. The `selectors` field should be an object similar to `reducers`, where the keys will be the selector function names, and the values are the selector functions to be generated.
+
+**Note that unlike writing a standalone selector function, the `state` argument to these selectors will be just the _slice state_, and _not_ the entire `RootState`!**.
+
+Here's what it might look like to convert the posts slice selectors to be defined inside of `createSlice`:
+
+```ts
+const postsSlice = createSlice({
+ name: 'posts',
+ initialState,
+ reducers: {
+ /* omit reducer logic */
+ },
+ // highlight-start
+ selectors: {
+ // Note that these selectors are given just the `PostsState`
+ // as an argument, not the entire `RootState`
+ selectAllPosts: postsState => postsState,
+ selectPostById: (postsState, postId: string) => {
+ return postsState.find(user => post.id === postId)
+ }
+ }
+ // highlight-end
+})
+
+export const { postAdded, postUpdated } = postsSlice.selectors
+
+export default postsSlice.reducer
+
+// highlight-start
+// We've replaced these standalone selectors:
+// export const selectAllPosts = (state: RootState) => state.posts
+
+// export const selectPostById = (state: RootState, postId: string) =>
+// state.posts.find(post => post.id === postId)
+
+// highlight-end
+```
+
+There _are_ still times you'll need to write selectors as standalone functions outside of `createSlice`. This is especially true if you're calling other selectors that need the entire `RootState` as their argument, in order to make sure the types match up correctly.
+
+
+
## Users and Posts
-So far, we only have one slice of state. The logic is defined in `postsSlice.js`, the data is stored in `state.posts`, and all of our components have been related to the posts feature. Real applications will probably have many different slices of state, and several different "feature folders" for the Redux logic and React components.
+So far, we only have one slice of state. The logic is defined in `postsSlice.ts`, the data is stored in `state.posts`, and all of our components have been related to the posts feature. Real applications will probably have many different slices of state, and several different "feature folders" for the Redux logic and React components.
-You can't have a "social media" app if there aren't any other people involved. Let's add the ability to keep track of a list of users in our app, and update the post-related functionality to make use of that data.
+You can't have a "social media" app if there aren't any other people involved! Let's add the ability to keep track of a list of users in our app, and update the post-related functionality to make use of that data.
### Adding a Users Slice
Since the concept of "users" is different than the concept of "posts", we want to keep the code and data for the users separated from the code and data for posts. We'll add a new `features/users` folder, and put a `usersSlice` file in there. Like with the posts slice, for now we'll add some initial entries so that we have data to work with.
-```js title="features/users/usersSlice.js"
-import { createSlice } from '@reduxjs/toolkit'
+```ts title="features/users/usersSlice.ts"
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+
+import type { RootState } from '@/app/store'
+
+interface User {
+ id: string
+ name: string
+}
-const initialState = [
+const initialState: User[] = [
{ id: '0', name: 'Tianna Jenkins' },
{ id: '1', name: 'Kevin Grant' },
{ id: '2', name: 'Madison Price' }
@@ -450,18 +619,23 @@ const usersSlice = createSlice({
})
export default usersSlice.reducer
+
+export const selectAllUsers = (state: RootState) => state.users
+
+export const selectUserById = (state: RootState, userId: string | null) =>
+ state.users.find(user => user.id === userId)
```
For now, we don't need to actually update the data, so we'll leave the `reducers` field as an empty object. (We'll come back to this in a later section.)
As before, we'll import the `usersReducer` into our store file and add it to the store setup:
-```js title="app/store.js"
+```ts title="app/store.ts"
import { configureStore } from '@reduxjs/toolkit'
-import postsReducer from '../features/posts/postsSlice'
+import postsReducer from '@/features/posts/postsSlice'
// highlight-next-line
-import usersReducer from '../features/users/usersSlice'
+import usersReducer from '@/features/users/usersSlice'
export default configureStore({
reducer: {
@@ -472,25 +646,43 @@ export default configureStore({
})
```
+Now, the root state looks like `{posts, users}`, matching the object we passed in as the `reducer` argument.
+
### Adding Authors for Posts
-Every post in our app was written by one of our users, and every time we add a new post, we should keep track of which user wrote that post. In a real app, we'd have some sort of a `state.currentUser` field that keeps track of the current logged-in user, and use that information whenever they add a post.
+Every post in our app was written by one of our users, and every time we add a new post, we should keep track of which user wrote that post. This will need changes for both the Redux state and the `` component.
-To keep things simpler for this example, we'll update our `` component so that we can select a user from a dropdown list, and we'll include that user's ID as part of the post. Once our post objects have a user ID in them, we can use that to look up the user's name and show it in each individual post in the UI.
+First, we need to update the existing `Post` data type to include a `user: string` field that contains the user ID that created the post. We'll also update the existing post entries in `initialState` to have a `post.user` field with one of the example user IDs.
-First, we need to update our `postAdded` action creator to accept a user ID as an argument, and include that in the action. (We'll also update the existing post entries in `initialState` to have a `post.user` field with one of the example user IDs.)
+Then, we need to update our existing reducers accordingly The `postAdded` prepare callback needs to accept a user ID as an argument, and include that in the action. Also, we _don't_ want to include the `user` field when we update a post - the only things we need are the `id` of the post that changed, and the new `title` and `content` fields for the updated text. We'll define a `PostUpdate` type that contains just those three fields from `Post`, and use that as the payload for `postUpdated` instead.
+
+```ts title="features/posts/postsSlice.ts"
+export interface Post {
+ id: string
+ title: string
+ content: string
+ user: string
+}
+
+// highlight-start
+type PostUpdate = Pick
+
+const initialState: Post[] = [
+ { id: '1', title: 'First Post!', content: 'Hello!', user: '0' },
+ { id: '2', title: 'Second Post', content: 'More text', user: '2' }
+]
+// highlight-end
-```js title="features/posts/postsSlice.js"
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
postAdded: {
- reducer(state, action) {
+ reducer(state, action: PayloadAction) {
state.push(action.payload)
},
// highlight-next-line
- prepare(title, content, userId) {
+ prepare(title: string, content: string, userId: string) {
return {
payload: {
id: nanoid(),
@@ -501,49 +693,50 @@ const postsSlice = createSlice({
}
}
}
+ },
+ // highlight-next-line
+ postUpdated(state, action: PayloadAction) {
+ const { id, title, content } = action.payload
+ const existingPost = state.find(post => post.id === id)
+ if (existingPost) {
+ existingPost.title = title
+ existingPost.content = content
+ }
}
- // other reducers
}
})
```
Now, in our ``, we can read the list of users from the store with `useSelector` and show them as a dropdown. We'll then take the ID of the selected user and pass that to the `postAdded` action creator. While we're at it, we can add a bit of validation logic to our form so that the user can only click the "Save Post" button if the title and content inputs have some actual text in them:
-```jsx title="features/posts/AddPostForm.js"
-import React, { useState } from 'react'
+```tsx title="features/posts/AddPostForm.tsx"
// highlight-next-line
-import { useDispatch, useSelector } from 'react-redux'
+import { selectAllUsers } from '@/features/users/usersSlice'
-import { postAdded } from './postsSlice'
+// omit other imports and form types
-export const AddPostForm = () => {
- const [title, setTitle] = useState('')
- const [content, setContent] = useState('')
+const AddPostForm = () => {
+ const dispatch = useAppDispatch()
// highlight-next-line
- const [userId, setUserId] = useState('')
+ const users = useAppSelector(selectAllUsers)
- const dispatch = useDispatch()
+ const handleSubmit = (e: React.FormEvent) => {
+ // Prevent server submission
+ e.preventDefault()
- // highlight-next-line
- const users = useSelector(state => state.users)
+ const { elements } = e.currentTarget
+ const title = elements.postTitle.value
+ const content = elements.postContent.value
+ // highlight-next-line
+ const userId = elements.postAuthor.value
- const onTitleChanged = e => setTitle(e.target.value)
- const onContentChanged = e => setContent(e.target.value)
- // highlight-next-line
- const onAuthorChanged = e => setUserId(e.target.value)
+ // highlight-next-line
+ dispatch(postAdded(title, content, userId))
- const onSavePostClicked = () => {
- if (title && content) {
- // highlight-next-line
- dispatch(postAdded(title, content, userId))
- setTitle('')
- setContent('')
- }
+ e.currentTarget.reset()
}
// highlight-start
- const canSave = Boolean(title) && Boolean(content) && Boolean(userId)
-
const usersOptions = users.map(user => (