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
接受兩個參數
- action types (posts/fetchPosts)
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>
)
}
相關連結: