From 28b1859d19f3bee9dffc9b828baad67a9a0ff13f Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Sun, 12 May 2024 14:52:39 -0400 Subject: [PATCH] Update existing Part 3 content with TS info --- .../essentials/part-2-app-structure.md | 2 +- docs/tutorials/essentials/part-3-data-flow.md | 259 ++++++++++-------- .../essentials/working_post_list.png | Bin 51937 -> 42347 bytes 3 files changed, 152 insertions(+), 109 deletions(-) diff --git a/docs/tutorials/essentials/part-2-app-structure.md b/docs/tutorials/essentials/part-2-app-structure.md index a6351951de..2f7a764c2e 100644 --- a/docs/tutorials/essentials/part-2-app-structure.md +++ b/docs/tutorials/essentials/part-2-app-structure.md @@ -145,7 +145,7 @@ When we pass in an object like `{counter: counterReducer}`, that says that we wa 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 +### 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. diff --git a/docs/tutorials/essentials/part-3-data-flow.md b/docs/tutorials/essentials/part-3-data-flow.md index 2f5487be56..e632f9064a 100644 --- a/docs/tutorials/essentials/part-3-data-flow.md +++ b/docs/tutorials/essentials/part-3-data-flow.md @@ -235,15 +235,21 @@ export const useAppSelector = useSelector.withTypes() // highlight-end ``` +That completes the setup process. Let's start building the app! + ## Main Posts Feed The main feature for our social media feed app will be a list of posts. We'll add several more pieces to this feature as we go along, but to start off, our first goal is to only show the list of post entries on screen. ### 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. @@ -251,32 +257,44 @@ 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, 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 +import postsReducer from '@/features/posts/postsSlice' -export default configureStore({ +export const store = configureStore({ reducer: { + // highlight-next-line posts: postsReducer } }) @@ -290,18 +308,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. 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 => (
@@ -319,19 +342,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' @@ -340,20 +356,18 @@ function App() {
- + ( + element={ // highlight-start - + <> - + // highlight-end - )} - /> - - + } + > +
) @@ -376,58 +390,67 @@ 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: -```jsx title="features/posts/AddPostForm.js" -import React, { useState } from 'react' +```tsx title="features/posts/AddPostForm.tsx" +import React 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 onTitleChanged = e => setTitle(e.target.value) - const onContentChanged = e => setContent(e.target.value) + const { elements } = e.currentTarget + const title = elements.postTitle.value + const content = elements.postContent.value + + console.log('Values: ', { title, content }) + + e.currentTarget.reset() + } return (

Add a New Post

-
+ - + -