diff --git a/docs/tutorials/essentials/part-2-app-structure.md b/docs/tutorials/essentials/part-2-app-structure.md index 042577e266..9721493e04 100644 --- a/docs/tutorials/essentials/part-2-app-structure.md +++ b/docs/tutorials/essentials/part-2-app-structure.md @@ -24,22 +24,25 @@ 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 @@ -56,6 +59,7 @@ On the right, we can see that our Redux store is starting off with an app state { counter: { value: 0 + status: 'idle' } } ``` @@ -111,55 +115,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, @@ -212,20 +233,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 @@ -233,14 +268,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 ``` @@ -254,9 +292,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. @@ -278,7 +316,7 @@ console.log(newState) // {value: 11} ``` -### Rules of Reducers +## Rules of Reducers We said earlier that reducers must **always** follow some special rules: @@ -382,16 +420,15 @@ But, here's something _very_ important to remember: 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 @@ -399,7 +436,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 } @@ -411,12 +449,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 + +**[TODO]** We'll learn more about selector functions in [Part 4: Using Redux Data](./part-4-using-data.md), and look at how they can be optimized in [Part 6: Performance](./part-6-performance-normalization.md) + +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 @@ -430,34 +504,42 @@ A **thunk** is a specific kind of Redux function that can contain asynchronous l 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 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 @@ -469,7 +551,53 @@ 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' + }) + } +}) +``` @@ -512,59 +640,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 */}
) } @@ -582,20 +727,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 @@ -605,14 +741,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. @@ -629,16 +765,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?" @@ -647,9 +800,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 (
@@ -661,13 +816,13 @@ return ( /> @@ -696,29 +851,34 @@ 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 + ) ``` @@ -745,11 +905,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-5-async-logic.md b/docs/tutorials/essentials/part-5-async-logic.md index 5f2054e6a6..bd4737a919 100644 --- a/docs/tutorials/essentials/part-5-async-logic.md +++ b/docs/tutorials/essentials/part-5-async-logic.md @@ -326,7 +326,7 @@ export const SinglePostPage = () => { } ``` -```js title="features/posts/EditPostForm.js" +```ts title="features/posts/EditPostForm.tsx" // omit imports //highlight-next-line import { postUpdated, selectPostById } from './postsSlice' diff --git a/docs/tutorials/essentials/part-6-performance-normalization.md b/docs/tutorials/essentials/part-6-performance-normalization.md index a3dd7a41a5..10527025ce 100644 --- a/docs/tutorials/essentials/part-6-performance-normalization.md +++ b/docs/tutorials/essentials/part-6-performance-normalization.md @@ -535,7 +535,7 @@ export const Navbar = () => { } ``` -Lastly, we need to update `App.js` with the "Notifications" route so we can navigate to it: +Lastly, we need to update `App.ts` with the "Notifications" route so we can navigate to it: ```tsx title="App.tsx" // omit imports diff --git a/docs/tutorials/essentials/part-7-rtk-query-basics.md b/docs/tutorials/essentials/part-7-rtk-query-basics.md index 34dcd5315a..660b059d24 100644 --- a/docs/tutorials/essentials/part-7-rtk-query-basics.md +++ b/docs/tutorials/essentials/part-7-rtk-query-basics.md @@ -228,7 +228,7 @@ In this case, our endpoint is `getPosts` and it's a query endpoint, so the gener ### Configuring the Store -We now need to hook up the API slice to our Redux store. We can modify the existing `store.js` file to add the API slice's cache reducer to the state. Also, the API slice generates a custom middleware that needs to be added to the store. This middleware _must_ be added as well - it manages cache lifetimes and expiration. +We now need to hook up the API slice to our Redux store. We can modify the existing `store.ts` file to add the API slice's cache reducer to the state. Also, the API slice generates a custom middleware that needs to be added to the store. This middleware _must_ be added as well - it manages cache lifetimes and expiration. ```ts title="app/store.ts" import { configureStore } from '@reduxjs/toolkit'