返回
Featured image of post React - Redux

React - Redux

React - Redux

Redux是個非必要工具,可以直接寫React管理專案即可

基本安裝

npx create-react-app my-app --template redux

官方手把手教程

官方連結

前置插件

Configuring Your Store

src/index.js

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';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}> // react-redux 確保任何時候通過 react-redux connect 在App中連接到 Redux 時,我們的comporent都可以使用store。
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
serviceWorker.unregister();

src/index.js

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';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}> // react-redux 確保任何時候通過 react-redux connect 在App中連接到 Redux 時,我們的comporent都可以使用store。
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);
serviceWorker.unregister();

/app/store

import { configureStore } from '@reduxjs/toolkit'; 
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({ // @reduxjs/toolkit 更簡化原先 Redux 文件所基本的配置方式
  reducer: {
    counter: counterReducer, // 連結到 Reducer
  },
});

快速入門

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
    }
  }
})

// 將 reducer 註冊至 action
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer

features/counter/Counter.js

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

export function Counter() {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch() // dispatch 分發 action

  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>
  )
}

Redux 使用情境

完全建議於gloabl層級使用,例如:購物車功能,就蠻需要這項武器的。

  • App許多地方都需要大量的狀態更新
  • update該狀態Function可能很複雜
  • App具有中型或大型代碼庫,可能有很多人在使用

不然其實畫面 component 一般用 React-Hook 就可以輕鬆解決傳遞問題

Redux 運作模式

它是一個獨立的App,包含以下部分:

  • The state, 是 App 預設 data
  • The view, 是 App 介面工具 UI
  • The actions, 是 用戶 於 App 觸發 Function

async await Redux 非同步的方式

非同步方式的目的就是為了達到,與API交握確定得到資料後進行UI處理,其實腳手架已經有實作一個簡單的方式呈現出來了。

// 不用複製這段 code 腳手架自動生成的 code
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { fetchCount } from './counterAPI';
// ... 略過

// 直接 export incrementAsync 出去給予使用
// 使用到 `createAsyncThunk` 
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (amount) => {
    const response = await fetchCount(amount);
    return response.data;
  }
);

export const counterSlice = createSlice({
// ... 略過  
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      });
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// ... 略過  

export default counterSlice.reducer;

一般建議 state 要有這兩個參數

{
  status: 'idle' | 'loading' | 'succeeded' | 'failed',  // '空閒'|'讀取'|'成功'|'失敗'
  error: string | null
}

createAsyncThunk 接受兩個參數

  1. action types (posts/fetchPosts)
  2. payload creator 要使用 callback Function,需 return data 或 reject Promise error (try/catch)

extraReducers 兩種使用方式

  • builder.addCase()
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
      // 省略 reducers
  },
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      });
  },
});
  • pending / fulfilled / rejected
const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // omit existing reducers here
  },
  extraReducers: {
    [fetchPosts.pending]: (state, action) => {
      state.status = 'loading'
    },
    [fetchPosts.fulfilled]: (state, action) => {
      state.status = 'succeeded'
      state.posts = state.posts.concat(action.payload)
    },
    [fetchPosts.rejected]: (state, action) => {
      state.status = 'failed'
      state.error = action.error.message
    }
  }
})

如何在 React Function 中使用 ? async await Redux 非同步的方式

這應該算是最常用到的技能了吧?交握API~

export const PostsList = () => {
  const dispatch = useDispatch()
  const posts = useSelector(selectAllPosts)

  const postStatus = useSelector(state => state.posts.status)
  const error = useSelector(state => state.posts.error)

  // 這邊蠻重要的,只要是狀態為空閒的時候就會自動執行一次 fetchPosts,所以剛進畫面的時候就會馬上執行一次
  useEffect(() => {
    if (postStatus === 'idle') {
      dispatch(fetchPosts())
    }
  }, [postStatus, dispatch])

  let content // let 一個參

  if (postStatus === 'loading') {  // 時機為 AsyncThunk.pending
    content = <div className="loader">Loading...</div>
  } 
  else if (postStatus === 'succeeded') { // 時機為 AsyncThunk.rejected
    // 處理回傳之值
    const orderedPosts = posts
      .slice()
      .sort((a, b) => b.date.localeCompare(a.date))

    content = orderedPosts.map(post => (
      <PostExcerpt key={post.id} post={post} />
    ))
  } 
  else if (postStatus === 'failed') { // 時機為 AsyncThunk.fulfilled 
    content = <div>{error}</div>
  }

  return (
    <section className="posts-list">
      <h2>Posts</h2>
      {content} // 模板值塞入
    </section>
  )
}

Redux 運作模式
Redux 運作模式


相關連結:

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus