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 (
// highlight-start // highlight-end - {count} + + {count} + + {/* omit additional rendering output here */}
- {/* omit additional rendering output here */}
) } @@ -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() {
- + ( + element={ // highlight-start - + <> - + // highlight-end - )} - /> - - + } + > +
) @@ -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

-
+ - +