# ⚪️ React Redux を基礎から理解する

# Redux の基本概念

  • Redux Overview:

    • Redux serves as an alternative to React Context, offering a centralized data store for the entire application.
    • The store manages all states, eg. critical data like authentication status and user input states.
    • It provides a unified state management approach by acting as a single store applicable to the entire application.

    redux

  • Subscription to Central Store:

    • Components subscribe to the central store for updates.
    • Subscribed components receive notifications when data changes occur in the Redux store.
    • Components selectively retrieve needed data, such as authentication status, from specific portions of the Redux store.
    • This establishes a unidirectional flow, enabling components to access and utilize data provided by the Redux store.
  • Important Rule:

    • Components refrain from direct data manipulation.
    • Subscriptions are established for components to receive updates, ensuring an indirect and controlled data flow.
    • Components avoid engaging in a direct data flow towards the storage.
  • Introducing Reducers:

    • Reducers handle data mutations and changes within the stored data.
    • The term "reducer" in Redux differs from the reducer hook in React.
    • A reducer function takes input, transforms it, and produces a new output, adhering to general programming concepts.
  • Understanding Reducer Functions:

    • Redux reducer functions accept input, transform it, and generate a new output.
    • They play a crucial role in updating stored data, following general programming principles.
  • Components and Actions:

    • Components do not directly manipulate stored data; they use subscriptions for updates.
    • Actions, triggered by components, describe the type of operation a reducer should execute.
    • Components dispatch actions as simple JavaScript objects, specifying the operation type.
    • Redux forwards actions to the appropriate reducer, which performs the specified operation.
    • The reducer outputs a new state, effectively replacing the existing state in the central data store.
    • Subscribed components receive notifications of state updates, facilitating UI refresh.
  • Three Principles of Redux:

    1. Single Data Source: The entire application's state is stored in a single store.
    2. State is Read-Only: State changes occur only through triggering actions, containing a required type attribute.
    3. Use Pure Functions for Actions: Reducers, implemented as pure functions, describe how actions change the state tree.

redux

  • Store and Reducer Relationship:

    • The store manages data, and its content is determined by the reducer function.
    • The reducer function produces a new state snapshot whenever an action reaches it.
    • Upon the initial code execution, the reducer also executes with a default action, typically setting the initial state.
    • It is crucial for the reducer function to return a new state object consistently.
    • The reducer function is a standard JavaScript function, always receiving the old/existing state and the dispatched action.
  • Reducer Function Structure:

    • The reducer function is a JavaScript function, typically created using arrow function syntax.
    • It must always return a new state object, ensuring it adheres to the principles of a pure function.
    • Pure functions guarantee that the same inputs yield the same outputs and have no internal side effects.
  • Default State in Reducer:

    • During the first execution, when the store initializes, the state might be undefined.
    • Provide a default value for the state parameter in the reducer function to handle this initial case.
    • The default value ensures that the state is set to an initial value, preventing undefined errors.
  • Creating and Subscribing to Store:

    • Components subscribe to the store using a subscriber function.
    • The subscriber function is notified whenever data and the store change.
    • Use the subscribe method on the store, passing the subscriber function, to establish the subscription.
  • Dispatching Actions:

    • Dispatch actions using the dispatch method on the store.
    • Actions are JavaScript objects with a type attribute acting as an identifier.
    • The type attribute should be a unique string, representing the type of action to be performed.
  • Sample Action and State Update:

    • Example of dispatching an action to increment a counter.
    • The dispatched action contains a type attribute indicating an increment action.
    • The reducer function interprets the action type and produces a new state with an incremented counter.
    • Subscribed components are notified of the state update.
  • Understanding Output:

    • Executing the code demonstrates the incrementing counter as actions are dispatched.
    • The store initialization and dispatching actions lead to state updates reflected in the output.

# Redux's Core API:

  1. Redux.createStore(reducer, [preloadedState], [enhancer])
  2. store.dispatch(action)
  3. store.subscribe(listener)
  4. store.getState()
  5. store.replaceReducer(nextReducer)

# createStore

createStore(reducer, [preloadedState], [enhancer])

  • Creates a Redux store that holds the complete state tree of the app.

  • There should only be a single store in the app.

  • createStore accepts three parameters:

    • reducer: A reducing function that returns the next state tree, given the current state tree and an action to handle.
    • [preloadedState]: The initial state.
    • [enhancer]: The store enhancer. You may optionally specify it to enhance the store with third-party capabilities such as middleware, time travel, persistence, etc. The only store enhancer that ships with Redux is applyMiddleware().

store/index.js

import { createStore } from "redux";
const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === "increment") {
    return {
      counter: state.counter + 1,
    };
  }
  if (action.type === "decrement") {
    return {
      counter: state.counter - 1,
    };
  }
  return state;
};
const store = createStore(counterReducer);
export default store;

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";

import "./index.css";
import App from "./App";
import store from "./store/index";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

Counter.js

import classes from "./Counter.module.css";
import { useSelector, useDispatch } from "react-redux";

const Counter = () => {
  // Again, this function will be executed for us by React Redux. it will then pass the Redux state in order to manage the data into this function when it is executed, and then basically execute this code to retrieve the state portion of this component that is needed. then use select or overall to return the value. useSelector((state) => state.counter);
  const counter = useSelector((state) => state.counter);
  // Redux automatically sets up a subscription to the Redux store for this component. So whenever the data in the Redux store changes, your component will be updated and automatically receive the latest counter. So it's an automatic reaction that changes to the Redux store will cause this component function to be re-executed. So you'll always have the most up-to-date counter.

  const dispatch = useDispatch();
  // dispatch is a function we can call, that will dispatch an action on our Redux store.
  const decrementHandler = () => {
    dispatch({ type: "decrement" });
  };

  const toggleCounterHandler = () => {};

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      <div className={classes.value}>{counter}</div>
      <div>
        <button onClick={incrementHandler}>Increment</button> 
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

# useSelector

In React Redux, the useSelector hook requires two parameters:

  1. Selector Function:

    • This is a function that takes the entire Redux state as a parameter and returns the portion of the state you want to extract. For example, if you have a Redux state tree with an object named stock, and within the stock object, there's a property named counter, your selector function might be (state) => state.stock.counter. This function determines the data that useSelector will return.
  2. Equality Function (Optional):

    • This is an optional parameter used to compare the values returned by the selector function in consecutive calls to determine if the component should be re-rendered. If omitted, useSelector will use reference equality (===), meaning it will re-render only if the object references from the previous and current calls are the same. If you need to customize when the component should re-render based on some condition, you can provide a custom equality function.

For example:

const counter = useSelector(
  (state) => state.stock.counter,
  (prev, next) => prev === next
);

In this example, the first parameter is the selector function, extracting state.stock.counter. The second parameter is a custom equality function that uses reference equality to determine whether to re-render the component based on the returned values from the selector function.

# dispatch

const dispatch = useDispatch();
// dispatch is a function we can call, that will dispatch an action on our Redux store.
const decrementHandler = () => {
  dispatch({ type: "decrement" });
  //Attaching Payloads to Actions
};

# Redux Toolkit (opens new window)

When our application grows in complexity, using Redux can become more intricate. In this course, we'll explore a simpler approach to utilizing Redux. Before we proceed, let's consider some potential issues:

Potential Issues:

  1. Action Type Identifiers:

    • As the application grows, there may be numerous operations, leading to confusion with identifiers.
    • Issues such as misspelling or conflicts in identifiers might arise.
  2. Data Volume Management:

    • With an increase in data volume, the state object becomes larger.
    • The Reducer function becomes more complex and may be challenging to maintain.
  3. Respecting State Immutability:

    • Ensuring the consistent return of a new state snapshot and avoiding unintentional alterations to the existing state.
    • Complex nested object and array data can lead to unpredictable state changes.

Solutions:

  1. Unique Identifier Issue:

    • Use constants to store identifiers, avoiding spelling errors and ensuring type consistency.
  2. Data Volume Management and Complex Reducer Issue:

    • Redux Toolkit provides solutions, such as splitting the Reducer into smaller ones.
  3. State Immutability Issue:

    • Manual implementation of solutions is possible, or Redux Toolkit tools can be used to automate state copying, ensuring unintentional state edits are avoided.

# Redux Toolkit's Core APIs:

  • configureStore(): wraps createStore to provide simplified configuration options and good defaults. It can automatically combine your slice reducers, adds whatever Redux middleware you supply, includes redux-thunk by default, and enables use of the Redux DevTools Extension.

  • createReducer(): that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements. In addition, it automatically uses the immer library to let you write simpler immutable updates with normal mutative code, like state.todos[3].completed = true.

  • createAction(): generates an action creator function for the given action type string. The function itself has toString() defined, so that it can be used in place of the type constant.

  • createSlice(): accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.

  • createAsyncThunk: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches pending/fulfilled/rejected action types based on that promise

# State management with Redux

  1. Redux Store Setup:

    • Purpose: This section is responsible for setting up the global Redux store using the configureStore function from the @reduxjs/toolkit library. The store is configured with the combined reducers that handle different parts of the application state.
  2. Wrap the App with the Redux Provider:

    • Purpose: The Provider component from react-redux is wrapped around the main application component (in this case, App). This ensures that the Redux store is accessible to all components within the application. It essentially "provides" the Redux store to the entire component tree.
  3. Creating a Redux Slice:

    • Purpose: A Redux slice is a unit of the Redux state that corresponds to a specific feature or part of the application. In this example, the exampleSlice manages a list of items with corresponding actions like adding and removing items. It provides a clean and organized way to define the initial state and actions related to a specific feature.
  4. Using Redux in a Component:

    • Purpose: This section demonstrates how to use the useSelector hook to access the Redux store's state within a React component. In the example, SomeComponent uses useSelector to retrieve a list of items from the Redux store and render them.
  5. Dispatching Actions in Another Component:

    • Purpose: This section shows how to use the useDispatch hook to dispatch actions to the Redux store from a React component. In the example, AnotherComponent allows the user to input a new item, and when a button is clicked, the addItem action is dispatched to add the new item to the Redux store.

# Process of using Redux Toolkit

This setup provides a basic structure for handling state management with Redux in a JavaScript app. Action creators generate actions, reducers specify how the state should change, and the store manages the overall application state. The useSelector hook is used to access the state, and useDispatch is used to dispatch actions.

🟦 Create the Redux Store:

// store.js
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers"; // Import your combined reducers

const store = configureStore({
  reducer: rootReducer,
});

export default store;

🟦 Wrap the App with the Redux Provider:

// index.js or App.js
import React from "react";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";

const MainApp = () => {
  return (
    <Provider store={store}>
      <App />
    </Provider>
  );
};

export default MainApp;

🟦 Creating a Redux Slice:

// exampleSlice.js
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  entities: {},
  ids: [],
};

export const exampleSlice = createSlice({
  name: "example",
  initialState,
  reducers: {
    addItem: (state, action) => {
      const { id, data } = action.payload;
      state.entities[id] = data;
      state.ids.push(id);
    },
    removeItem: (state, action) => {
      const idToRemove = action.payload;
      delete state.entities[idToRemove];
      state.ids = state.ids.filter((id) => id !== idToRemove);
    },
  },
});

export const { addItem, removeItem } = exampleSlice.actions;
export default exampleSlice.reducer;

🟦 Using Redux in a Component:

// SomeComponent.js
import React from "react";
import { useSelector } from "react-redux";

function SomeComponent() {
  const items = useSelector((state) =>
    state.example.ids.map((id) => state.example.entities[id])
  );

  return (
    <div>
      {items.map((item) => (
        <div key={item.id}>{item.name}</div>
      ))}
    </div>
  );
}

export default SomeComponent;

🟦 Dispatching Actions in Another Component:

// AnotherComponent.js
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addItem } from "./exampleSlice";

function AnotherComponent() {
  const dispatch = useDispatch();
  const [newItem, setNewItem] = useState("");

  const handleAddItem = () => {
    const id = Math.random().toString(36).substring(7); // Generate a unique ID
    const data = { id, name: newItem }; // Example data structure
    dispatch(addItem({ id, data }));
    setNewItem("");
  };

  return (
    <div>
      <input
        type="text"
        value={newItem}
        onChange={(e) => setNewItem(e.target.value)}
      />
      <button onClick={handleAddItem}>Add Item</button>
    </div>
  );
}

export default AnotherComponent;

🔵 useSelector(): Accessing State

  • Purpose: useSelector is a React hook provided by the react-redux library. Its primary purpose is to select and access the current state from the Redux store. It takes a selector function as an argument, which allows you to extract specific pieces of data from the state. By using useSelector, components can efficiently subscribe to changes in the Redux state and re-render when relevant data is updated.

🔵 useDispatch(): Modifying State with Actions

  • Purpose: useDispatch is another React hook from the react-redux library. Its primary purpose is to provide a reference to the dispatch function of the Redux store. This allows components to dispatch actions, which are plain JavaScript objects containing a type property and, optionally, a payload. By using useDispatch, components can trigger state changes by dispatching actions. These actions are then processed by reducers, modifying the state in a predictable and controlled manner.

🔵 Summary:

  • Redux Store Setup: Configured a global store using configureStore from @reduxjs/toolkit.
  • Provider Usage: Wrapped the main application component with Provider to make the Redux store accessible throughout the component tree.
  • Redux Slice Creation: Created a Redux slice using createSlice to manage a list of items in the state.
  • Component Interaction: Used useSelector to access state data and useDispatch to dispatch actions from React components.

# Handling Asynchronous Code:

Redux Core Principle:

  • Reducer functions in Redux must be pure, without side effects, and synchronous.
  • Pure functions produce the same output for the same input, ensuring consistency and predictability.

Handling Asynchronous Code:

  • A challenge arises when dealing with asynchronous actions, like HTTP requests, in Redux.
  • Reducer functions are unsuitable for asynchronous code due to their synchronous nature.

Options for Handling Asynchronous Code:

  1. Component-Level Side Effects:

    • Place side effect code, including asynchronous operations, directly in the component.
    • Dispatch actions after the side effect completion to inform Redux.
  2. Custom Action Creators:

    • Write custom action creators for handling asynchronous tasks without altering the reducer function.
    • Allows running asynchronous tasks as part of the action creator.

Redux Toolkit Solution:

  • Redux Toolkit provides a solution for handling asynchronous tasks within action creators.
  • It allows the execution of side effects without violating the synchronous nature of reducer functions.

reduxasync

# Redux Thunk

Redux Thunk:

  • Redux Thunk is a middleware that allows the execution of asynchronous tasks in Redux.
  • It enables the dispatch of asynchronous actions, such as HTTP requests, in Redux.
export const sendCartData = (cart) => {
  //instead of return an action object, 如 { type: 'SOME_ACTION', payload: someData }。 我們創建一個一個action creator return another function
  return async (dispatch) => {
    dispatch(
      uiActions.showNotification({
        status: "pending",
        title: "Sending...",
        message: "Sending cart data!",
      })
    );

    const sendRequest = async () => {
      const response = await fetch(
        "https://react-http-6b4a6.firebaseio.com/cart.json",
        {
          method: "PUT",
          body: JSON.stringify(cart),
        }
      );

      if (!response.ok) {
        throw new Error("Sending cart data failed.");
      }
    };

    try {
      await sendRequest();

      dispatch(
        uiActions.showNotification({
          status: "success",
          title: "Success!",
          message: "Sent cart data successfully!",
        })
      );
    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: "error",
          title: "Error!",
          message: "Sending cart data failed!",
        })
      );
    }
  };
};

The provided code demonstrates the use of a Redux thunk as an action creator. In contrast to regular action creators, which directly return an action object (e.g., { type: 'SOME_ACTION', payload: someData }), this action creator returns a function.

Here are some key differences in this approach:

  1. Returning a Function Instead of a Direct Action Object:

    • Regular action creators typically return a plain action object. In this case, the action creator returns a function.
  2. Incorporating Asynchronous Logic:

    • The returned function contains asynchronous logic. In this example, it involves sending shopping cart data to a server using async/await with the fetch function to make a PUT request.
  3. Dispatching Actions Before and After Asynchronous Logic:

    • Before the asynchronous logic starts, an action is dispatched to indicate that the data is pending ("pending" notification). After the asynchronous logic succeeds or fails, corresponding actions are dispatched to display notifications with success or error messages.
  4. Error Handling:

    • Errors are handled using a try-catch block. If an error occurs during the asynchronous logic, an action is dispatched to show an "error" notification along with the appropriate error message.
  5. More Flexible Control Flow:

    • Thunks provide a more flexible control flow, allowing you to dispatch different actions at different stages of asynchronous logic. For instance, dispatching a "pending" action at the start, a "success" action upon success, and an "error" action upon failure.

In summary, this approach enables action creators to execute more complex logic, including asynchronous operations, conditional checks, and error handling. Thunks allow you to abstract away this logic from the components, allowing them to focus on user interface interactions without having to deal directly with complex asynchronous or side-effect logic.