react教學系列

這是react超推薦學習的管理套件,在很多重複頁面都會用到的狀態上,非常好管理,可以大大減少維護的成本,以及提高程式效率,只是剛開始學習會花一些時間理解他的整體架構,需要一點耐心了解

※ps整篇文章依照布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI的課程學習,範例也是依照課程的,所以文章敘述都是單獨部分的觀念,快速架設Redux參考下面文章

Redux運作基本概念

類似郵差(Dispatch)帶著包裹(Action)送給收信、處理者(reducer)

reducer是一個switch case的function, 處理action帶來的邏輯,更新state  
Action是描述動作,帶了什麼資料,是物件。EX:取錢、並取10元  
Dispatch派發action給reducer的人  
Store包含Reducer跟State,通知UI要重新渲染

安裝Redux

來到官網,可以看到有三個工具

Redux核心的函式庫、工具
React-Redux透過提供的API(Hook)去將Redux連結我們APP(react組件),是React跟Redux的橋樑
Redux-Toolkit是讓整個寫法更簡潔,在沒有這套工具的時候一種狀態會分為reducer、Action、Dispatch三個部份三個檔案,所以這是一套檔案管理工具

然後點擊Get Started,右邊有Installation,安裝Redux Toolkit,那這裡面已經包含Redux Core,所以Redux Core就不用再安裝

npm install @reduxjs/toolkit

再來安裝react-redux跟redux-devtools,這個devtools是一個可以從chrome的插件去管理redux狀態的一個工具

npm install @types/react-redux
npm install --save-dev @redux-devtools/core

google搜尋Redux DevTools,就可以到chrome的線上應用程式找到這個插件

安裝後就可以從開發者工具裡面,找到Redux,這樣開發的時候就可以查看每個狀態

專案練習

接下來我要透過布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI這堂課提供的版型,練習用redex建立一個TodoList

Slice.ts的建立

  • 先到官網的教學文件,快速建立中,將Slice建立起來
  • 將官網的例子複製
  • 反白的部分需要依照自己需求建立
  • 將reducers裡面的邏輯刪除
  • 那在建立createSlice的時候,有三個參數
    • 參數1:name >取名子
    • 參數2:initialState > 建立初始化的state,將反白的那段型態跟內容定義好
    • 參數3:reducers > 處理Dispatch傳來的東西
      • 建立的function有兩個參數
      • 參數1:上面設定的state
      • Dispatch傳來的內容payload
  • 最後export,把剛剛設定的Slice.actions設定出來,從中取出設好的reducers方法(這些功能會在UI [組件]那裡調用)
  • 還有export,預設為Slice.reducer,會在store那邊import進去用

建立Store.ts

  • 繼續官網的教學文件,將Store建立起來
  • 複製官網的import例子,利用configureStore建立store
  • configureStore的參數為一個物件,必須放入reducer
  • 再將我們剛剛這設置好的Slice檔案裡的reducer給import進來給reducer
  • 再將store給export出去

UI使用Store

將store引入到UI (組件)裡面使用,就先回到源頭index.tsx裡面

  • 把react-redux中的Provider給import進來
  • 使用Provider把APP組件包住
  • 剛設定好的store給import進來
  • 設定Provider的屬性store為import來的store

有點類似context的用法,那react-redux這就是用來溝通react跟redux的橋樑,所以要找兩者相通的API就要從這裡找

APP被帶有store屬性的Provider包覆,所以APP所有的子組件都可以調用store設定的狀態

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import store from "./store";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

這邊設定好之後,打開開發者工具選擇redex就可以看到state裡面,有你設定的store裡的state

PS:我練習專案畫面為TypeScript + React 全攻略|快速上手仿 Instagram UI的課程版型

調用Store

剛已經建立了兩者的關聯,那要調用裡面的state(todoList)的話,就要使用API,那redux提供的Hook因為是TypeScript的關係,要先設定一些型態的部分,所以先設定hook.ts的檔案

建立hook.ts

這個檔案完全是為了設定型態而建立,不然useSelector是可以直接取用

  • 繼續官網的教學文件,將hook.ts建立起來
  • 先將react-redux中的useSelector跟TypedUseSelectorHook給import進來
    • useSelector 取得當下store的state(todoList)的內容
    • TypedUseSelectorHook是用來設定型態用的API
  • 建立一個useAppSelector 給予值就是useSelector,型態就要從Store取得
  • 到Store檔案中
    • 設定RootState型態,並使用ReturnType把Store設定為型態取出來
    • 並將RootState給export出來
  • 回到hook.ts檔案中把Store的RootState這個型態import進來

在UI使用State

剛剛設定好hooks之後,接下來只要在Provider包覆之下的組件都可以使用這個API,那就先在APP上面使用看看

先將hook的useAppSelector給import進來

直接使用這個API取的狀態在參數,就可以回傳store裡面的reducer

import { useAppSelector } from "./hook";
function App() {
  const todoList = useAppSelector((state) => {
    return state.todoReducer;
  });

    console.log(todoList);	
	return (    <></>);
}

建立Dispatch

接下來要讓UI觸發 Dispatch發一些action給reducer去更新狀態,所以回到hook檔案把剛剛沒建立的Dispatch這個API給建立好

  • 繼續官網的教學文件,將hook.ts的Dispatch建立起來
  • useDispatch給import進來
  • 一樣賦予型態,那型態也是從Store去取得
  • 回到store.ts
  • 建立AppDispatch型態,從store裡面取得dispatch的型態,最後export
  • 回到hook.ts把AppDispatch從store給import進來
  • 建立一個useAppDispatch,從useDispatch設定型態給予

讓UI發起Dispatch

建立好dispatch之後,就要讓UI有發出Dispatch的功用

※官方範例slices的action應該是increment, decrement, incrementByAmount這些,只是我這裡用TypeScript + React 全攻略|快速上手仿 Instagram UI這堂課練習

  • 先把Dispatch的hook跟之前在slices定義好的action給import進來
  • 宣告dispatch使用useAppDispatch這個hook
  • 直接在UI的click上使用dispatch()
  • dispatch()參數為之前slices設定好的action方法
import { useAppSelector ,useAppDispatch } from "./hook";
import { addTodo, addTimeStamp } from "./slices/todo";
function App() {
  const todoReducer = useAppSelector((state) => {
    return state.todoReducer;
  });
  const todoList = todoReducer.todoList;

  const dispatch = useAppDispatch();

  return (
<button  
onClick={() => {
          dispatch(addTimeStamp());
        }}
}></button>
);
}

中間層Middleware

從Dispatch到渲染之間,所做的邏輯處理

先找到官網的Understanding Redux裡面的History and Design有找到Middleware

先建立一個檔案middleware.ts

把官網的範例貼上來

※因為是TypeScript的關係,所以把Middlewarek的型態加入進去

import { Middleware } from "redux";

export const logger: Middleware = (store) => (next) => (action) => {
  console.log("dispatching", action);
  let result = next(action);
  console.log("next state", store.getState());
  return result;
};

然後在store.ts將middleware給引入

  • 把middleware給import進來之後
  • 並在reducer下方新增一個middleware:
    • 這裡有兩個方法一個直接放陣列後把值丟進去
    • 另一個是把funtion寫出來,參數會抓到middleware陣列,並且把我們自訂義的middleware加進去
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger } from "./middleware";

const store = configureStore({
  reducer: {
    todoReducer,
  },
  // middleware:[logger]
  middleware: (getMiddleware) => {
    return getMiddleware().concat(logger);
  },
});

export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

接下來要解決我們設定的middleware類型報錯問題

回到官網的Using Redux的code Quality裡面的Usage With TypeScript,這個是專門找一些寫TypeScript會遇到的雷區問題

那可以看到右邊有個Type Checking Middleware這個問題區

可以看到,官網這邊是說

  • 把Redux 的Middleware跟store的RootState分別import進來之後
  • 把自訂義的Middleware function加上型態就可以解決

※但實際上還是有報錯

主要是我們RootState的部分是用Store本身去推斷型態,所以往下看還有其他解法

import { Middleware } from "redux";
import { RootState } from "./store";

export const logger: Middleware<
  {}, // Most middleware do not modify the dispatch return value
  RootState
> = (store) => (next) => (action) => {
  console.log("dispatching", action);
  let result = next(action);
  console.log("next state", store.getState());
  return result;
};

往下移就能看到,使用combineReducers把Reducer集結起來,並且重新給予RootState型態就好

  • 先combineReducers從Redux給import進來
  • 將reducer給集結起來
  • 把reducer的值改為集結後的rootReducer
  • 再把RootState改為取得rootReducer的型態
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger } from "./middleware";
import { combineReducers } from "redux";

const rootReducer = combineReducers({ todoReducer });

const store = configureStore({
  reducer: rootReducer,
  // middleware:[logger]
  middleware: (getMiddleware) => {
    return getMiddleware().concat(logger);
  },
});

export default store;
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

新增多個middleware

那就是在middleware檔案中,依照範例在新增一個function

  • 並到store.ts將function給import進來
  • 再多新增一個concat即可
export const loggerMyName: Middleware<
  {}, // Most middleware do not modify the dispatch return value
  RootState
> = (store) => (next) => (action) => {
  console.log("dispatching , I am Hank");
  let result = next(action);
  console.log("dispatching之後 , I am Mary");
  return result;
};

store.ts

import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger, loggerMyName } from "./middleware";
import { combineReducers } from "redux";

const rootReducer = combineReducers({ todoReducer });
const store = configureStore({
  reducer: rootReducer,
  // middleware:[logger]
  middleware: (getMiddleware) => {
    return getMiddleware().concat(logger).concat(loggerMyName);
  },
});

export default store;
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

RTA Query

使用Redux Toolkit裡面有個功能是獲取後端的API,以前大家會使用React thunk來獲取API,但如果使用在redux就要寫很多padding、success、error在reducers裡面,透過RTA這套工具可以替我們處理這些狀態,以及融入狀態管理中

先到Redux Toolkit的官網的RTK Query裡面有RTK Query Overview,看到重點是使用createApi跟fetchBaseQuery來獲取需要的資料

  • 先建立一個檔案server資料夾裡面放todoAPI.ts
  • 將createApi跟fetchBaseQuery給import進來
  • 將官網的範例貼上
  • 使用https://jsonplaceholder.typicode.com/ 來練習
  • 最後將自訂義的hook給export出去
reducerPath就是幫我們寫好的reducer內容,並且包含padding、success、error這些功能,只需要callAPI就好,可以自己取名子  
baseQuery這個就是你要抓取的後端資料網址  
endpoints針對這個路徑,抓取底下更多內容。 使用builder的query跟baseUrl去搭配,就可以用最後輸出的hook去call資料   < Pokemon , string > Pokemon回傳資料的類型,這個自己定義 string 是到時候要輸入的網址後面的內容
Hook的部分會依照endpoints裡面的設定,取名為useXXXQuery
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
// import { Pokemon } from "./types";

export const todoApi = createApi({
  reducerPath: "todoApi",
  baseQuery: fetchBaseQuery({
    baseUrl: "https://jsonplaceholder.typicode.com/",
  }),
  endpoints: (builder) => ({
    // getPokemonByName: builder.query<Pokemon, string>({
    //   query: (name) => `pokemon/${name}`,
    todoList: builder.query<any, string>({
      query: (id) => `todos/${id}`,
    }),
  }),
});

export const { useTodoListQuery } = todoApi;

將建立好的API跟store做連結,回到store.ts檔案

先把剛剛建立的檔案所設定的API給import進來

  • 放進reducer裡面
    • 有建立combineReducers就放進combineReducers裡面
    • 拿出剛剛設定reducerPath,對應的值就是他們原本設定好的reducer
  • 再來是把middleware給設定好
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./slices/todo";
import { logger, loggerMyName } from "./middleware";
import { combineReducers } from "redux";
import { todoApi } from "./sever/todoAPI";

const rootReducer = combineReducers({
  todoReducer,
  [todoApi.reducerPath]: todoApi.reducer,
});

const store = configureStore({
  reducer: rootReducer,
  // middleware:[logger]
  middleware: (getMiddleware) => {
    return (
      getMiddleware()
        // .concat(logger)
        // .concat(loggerMyName)
        .concat(todoApi.middleware)
    );
  },
});

export default store;
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;

接下來使用設定好的hook,回到APP.tsx

  • 把剛剛設定的useTodoListQuery給import進來
  • 宣告使用hook來獲取三個原本設定好的方法,分別是資料,發生錯誤,是否載入中
  • 最後log出來看看內容是什麼

log的結果就是載入中顯示,true,則載入完畢看到是false,並且獲取到data

import {useTodoListQuery} from "./sever/todoAPI";
function App() {
  const { data, error, isLoading } = useTodoListQuery("1");
  console.log("data", data);
  console.log("error", error);
  console.log("isLoading", isLoading);
}

針對非TypeScript 建立Redux

2023/3/21更新

由於上述都是針對課程,是由TypeScript建立React,我想記錄一下用JS建立的Redux,以下範例直接參照官網設置 Quick Start,前面npm安裝的部分就不再贅述

※使用的是React 17版

建立store

  • 建立Store檔案
  • 使用configureStore來建立stroe,reducer的部分,counterReducer這個檔案等等會建立先引入

檔案位置:app/store.js

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export default configureStore({
  reducer: {
    counter: counterReducer
  }
})

建立counterSlice

  • 建立Slice檔案,這是要建立剛剛store的reducer
  • createSlice來建立,三個參數,分別為name、各狀態、及reducers(修改狀態的方法)

檔案位置:features/counter/counterSlice.js

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

串接置UI

index.js

引入到index.js,並且使用Provider,將store串接起來

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Counter.js

  • 使用useSelector取得剛設置的狀態,useDispatch傳遞UI指令
  • 引入剛剛設置的reducer方法decrement、increment,透過useDispatch來傳遞

檔案位置:features/counter/counterSlice.js

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'


export default function Counter() {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

App.js

  • 這邊就沒幹嘛,把剛剛設置的Counter組件引入
import logo from './logo.svg';
import './App.css';
import Counter from "./features/counter/Counter"
function App() {
  return (
    <div className="App">
      <Counter></Counter>
    </div>
  );
}

export default App;

結論

今天持續學習布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI,在學習Redux我是花比較多時間的,由於他分的步驟相對較多,必須很清楚知道每個步驟都在幹嘛,他又分別拆分為三個套件,在理解整體架構之後,才會完全了解三個套件分別在什麼時間點被使用,只能說透過課程學習減少了我自己學習的踩雷時間,這個紀錄依照學習課程記錄下來,單單從文章內很難架設完整(依照課程而非官網程式碼),但可以知道每個程式碼的觀念,而不是單純抄下官網的範例而已,要知道完整架設流程,可以參考下面這篇,是寫給我自己以後快速架設理解用的文章

布魯斯的 TypeScript + React 全攻略|快速上手仿 Instagram UI

By dong

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *