React Advanced II Notes

React Advanced II Course Introduction

Course Overview

  • The course covers advanced React topics, including state management with Redux, React testing, and server-side rendering (SSR).

Curriculum

  • Redux for State Management: Learn to integrate Redux with a React application, building upon the Flux pattern.
  • React Testing: Explore various methods for testing React components.
  • Server-Side Rendering (SSR): Learn to render pages using an SSR server to improve performance and SEO.

Recommended Background

  • Basic understanding of HTML, CSS, and JavaScript.
  • Experience creating simple UIs with React.
  • Interest in exploring related libraries in detail.
  • Experience building static pages with HTML/CSS and using JavaScript events to manipulate DOM elements.

Course Objectives

  1. Implementing Functionalities: Understand how to implement desired functionalities in React using related tools and libraries.
  2. Technology Selection: Identify and select appropriate technologies based on project goals.
  3. Understanding React Technologies: Recognize the purpose and necessity of each related React technology.

State Management Using Redux

Table of Contents
  1. Introduction to Redux
  2. Redux Structure
  3. Redux-toolkit Utilization
  4. Connecting Redux with React
  5. Handling Asynchronous Operations with Redux
Introduction to Redux
  • A library designed to manage the entire state of an application easily.
  • Redux borrows many concepts from the Flux pattern.
  • Primarily used with React applications.
  • Extensive documentation is available at redux.js.org, along with many online resources.
  • Numerous examples of Redux being used in real-world app development.
When Should Redux Be Used?
  • When managing the entire state of the application is necessary.
  • When complex asynchronous processing requires structured state management.
  • When the application state becomes too complex and needs a systematic approach.
  • When introducing a state management pattern to enable collaboration among multiple developers.
  • When tools like logger, devtools, and others are needed to effectively manage and monitor state changes.
Core Principles
  • Single Source of Truth: The entire application state is stored in a single store.
  • Immutability: The state is read-only; updates require creating a new state object.
  • Pure Functions: State changes are handled by pure functions (reducers) that produce no side effects.
Action
  • Represents a change in state.
  • Typically a JavaScript object containing a type and an optional payload.

    Example: javascript const action1 = { type: 'namespace/getMyData', payload: { id: 123 } }     

Action Creator
  • A function that returns an Action object.
  • Improves reusability and adds flexibility by enabling middleware or preprocessing logic.

    Example: javascript const addObj = (id) => ({ type: 'namespace/getMyData', payload: { id: String(id).slice(1) } })     

Store
  • Stores the state of the entire app.
  • The reducer creates a new state based on the Action, and the Store saves that state.
  • The state of the Store is immutable.

    Example: javascript const store = createStore(reducer, initialState)     

Reducer
  • Receives an Action and creates a new State.
  • Follows the interface: (state, action) => state.
  • Should have no side effects when changing the state.

    Example: javascript const reducer = (state, action) => { switch (action.type) { case 'namespace/getMyData': const obj = { id: action.payload.id } return { ...state, obj } default: return state } } const store = createStore(reducer, initialState)     

Dispatch
  • A function for sending Actions to Redux.
  • After dispatching, the action passes through the middleware and reaches the reducer.

    Example: javascript function MyApp() { const dispatch = useDispatch() return ( <button onClick={() => dispatch(addObj(1234))}>Submit</button> ) } &nbsp;&nbsp;&nbsp;&nbsp;

Selector
  • A function used to retrieve specific pieces of state from the Redux store.
  • Selectors can access raw state values or computed data.
  • Helps organize and optimize how state is accessed across components. javascript function MyApp() { const obj = useSelector(state => state.obj) return ( <div>{JSON.stringify(obj)}</div> ) } &nbsp;&nbsp;&nbsp;&nbsp;
Redux Structure
  • Redux is highly flexible and can be freely extended as needed.
  • Understanding how actions and data flow internally allows you to extend Redux using middleware, enhancers, and other tools.
Middleware
  • Intercepts actions after they are dispatched and before they reach the reducer.
  • Libraries like redux-thunk and redux-logger are commonly used.
Enhancer
  • Wraps the store and can modify how actions are processed, typically after all middleware has run and before reaching the reducer.
  • Libraries such as Redux DevTools are applied as enhancers.
Redux-toolkit Utilization
  • Redux Toolkit is the officially recommended helper library for Redux.
  • Eliminates much of the boilerplate code previously required.
  • Simplifies writing Redux logic.
  • Includes useful libraries by default: redux-devtools, immerjs, redux-thunk, and reselect.
Redux-toolkit API - configureStore
  • A wrapper around Redux’s createStore function.
  • Simplifies store creation using named parameters.
  • The reducer key accepts an object, and configureStore automatically applies combineReducers internally.

    Example: javascript const store = configureStore({ reducer: { posts: postsReducer, users: usersReducer } }) &nbsp;&nbsp;&nbsp;&nbsp;

Redux-toolkit API - createAction
  • An action creator is a function that returns an action object.
  • Data passed to the action creator is included in the payload field of the action.
  • The generated action creator overrides the toString() method to return the action’s type string.

    Example: javascript const addPost = createAction('post/addPost') addPost({ title: 'post 1' }) /* { type: 'post/addPost', payload: { title: 'post 1' } } */ &nbsp;&nbsp;&nbsp;&nbsp;

Redux-toolkit API - createReducer
  • A reducer is created using the createReducer function.
  • The builder.addCase method is used to define how the state should change for each specific action.
  • Internally, Immer.js is used, allowing you to write code that appears to mutate the state directly while maintaining immutability.

    Example: javascript const postsReducer = createReducer(initState, builder => { builder.addCase(addPost, (state, action) => { state.posts.push(action.payload) }) }) &nbsp;&nbsp;&nbsp;&nbsp;

Redux-toolkit API - createSlice
  • A slice combines action creators and reducers into a single object.
  • The createSlice function simplifies Redux logic by reducing boilerplate code.
  • It automatically generates the action creators and reducer based on the configuration provided.

    Example: javascript const postsSlice = createSlice({ name: 'posts', initialState, reducers: { addPost(state, action) { state.posts.push(action.payload) } } }) const { addPost } = postsSlice.actions const reducer = postsSlice.reducer &nbsp;&nbsp;&nbsp;&nbsp;

Redux-toolkit API - createSelector
  • The createSelector function is used to derive specific data from the Redux state.
  • It allows for memoized selectors— if the input data hasn’t changed, the cached result is returned instead of recalculating.

    Example: javascript const postsSelector = state => state.posts const userSelector = state => state.user const postsByUserIdSelector = createSelector( postsSelector, userSelector, (posts, user) => posts.filter(post => post.username === user.username) ) &nbsp;&nbsp;&nbsp;&nbsp;

Connecting Redux with React
  • A library used to connect Redux with a React application.
  • Allows components to access Redux-managed state and dispatch actions.
  • Compatible with both class components and functional components.
React-redux API - Provider
  • To connect the Redux store to a React application, the root component must be wrapped with the Provider component from react-redux.
  • Any component rendered within the Provider can access the Redux state and dispatch actions. javascript const store = configureStore({ reducer: rootReducer }) function App() { return ( <Provider store={store}> <MyPage /> </Provider> ) } &nbsp;&nbsp;&nbsp;&nbsp;
React-redux API - useDispatch
  • The useDispatch hook is used to access the Redux dispatch function.
  • By calling dispatch(action), the generated action is sent to the Redux store to update the state.
  • Typically used in response to user interactions, such as button clicks.

    Example: javascript const addPost = createAction('addPost') function MyPage() { const dispatch = useDispatch() const handleClick = () => dispatch(addPost()) return ( <button onClick={handleClick}>Submit</button> ) } &nbsp;&nbsp;&nbsp;&nbsp;

React-redux API - useSelector
  • The useSelector hook is used to retrieve data from the Redux store.
  • A selector function is passed as an argument to specify which part of the state to access.
  • The selector function must be pure and should not modify the state.
  • Data can be derived or transformed into a specific format before being returned.

    Example: javascript function MyPage() { const posts = useSelector(state => state.posts) return posts.map(post => <Post {...post} />) } &nbsp;&nbsp;&nbsp;&nbsp;

Handling Asynchronous Operations with Redux
  • To handle asynchronous processing in Redux, it is necessary to add middleware.
  • redux-thunk is a middleware that simplifies handling asynchronous actions by using Promises.
createAsyncThunk
  • redux-toolkit includes thunk middleware by default.
  • redux-toolkit provides the createAsyncThunk API for handling asynchronous actions.
  • It automatically manages three states: pending, fulfilled, and rejected.
  • In a TypeScript environment, using a builder callback when writing reducers is necessary to ensure accurate typing.
  • createAsyncThunk takes two arguments: an action type and an async callback (the payload creator).
  • When an action type is provided, it automatically generates three lifecycle action types: pending, fulfilled, and rejected. These are used as postfixes and dispatched to reducers. Example: posts/addPost/pending

    Example: ```javascript const addPost = createAsyncThunk('posts/addPost', async (title) => { const result = await PostAPI.addPost({ title }) return result.data })

// Component
useEffect(() => {
    dispatch(addPost("post 1"))
}, [])

    ```

  • Using createAsyncThunk, the created action creator consists of four functions:

    • addPost: An async function used to dispatch the action.
    • addPost.pending: Dispatched when the promise is still pending.
    • addPost.fulfilled: Dispatched when the promise resolves successfully.
    • addPost.rejected: Dispatched when the promise is rejected due to an error.
  • The extraReducers function of createSlice allows you to handle additional action types by adding them to the builder.

  • The builder pattern is officially recommended because it makes TypeScript typing easier and more manageable. javascript const postsSlice = createSlice({ // ... extraReducers: builder => { builder .addCase(addPost.pending, state => ...) .addCase(addPost.fulfilled, state => ...) .addCase(addPost.rejected, state => ...) } }) &nbsp;&nbsp;&nbsp;&nbsp;

  • When fulfilled, the data is received as the payload, e.g., action.payload.todos.

  • When rejected, the error is received as action.error, and in this case, action.payload becomes undefined.

Continuous Asynchronous Processing
  • When a thunk function is dispatched, it returns a Promise.
  • Therefore, the .then() method can be used to chain asynchronous operations sequentially.

    Example: javascript dispatch(addPost("post1")) .then(() => dispatch(updatePost("post2"))) &nbsp;&nbsp;&nbsp;&nbsp;

Concurrent Asynchronous Processing
  • By using Promise.all, multiple asynchronous operations can be executed concurrently.
  • Note: If any thunk’s Promise is rejected, the .then() block will not execute.

    Example: javascript Promise.all([ dispatch(addPost("post1")), dispatch(updatePost("post2")) ]) .then(() => console.log("DONE")) &nbsp;&nbsp;&nbsp;&nbsp;