# 組合 Redux actions 如果不堅持一定得用 `combineReducers` 的話, Redux action 還可以做到些奇怪的事。 例如平常寫 action 時為了讓 action 可以被 dispatch 到正確的地方,會寫: ```javascript const POST_REQUEST = 'POST_REQUEST'; const POST_SUCCESS = 'POST_SUCCESS'; const POST_FAILURE = 'POST_FAILURE'; const PRODUCT_LIST_REQUEST = 'PRODUCT_LIST_REQUEST'; const PRODUCT_LIST_SUCCESS = 'PRODUCT_LIST_SUCCESS'; const PRODUCT_LIST_FAILURE = 'PRODUCT_LIST_FAILURE'; const PostRequest = () => ({ type: POST_REQUEST, }); const PostSuccess = (data) => ({ type: POST_SUCCESS, data, }); const PostFailure = (error) => ({ type: POST_FAILURE, error, }); const ProductListRequest = () => ({ type: PRODUCT_LIST_REQUEST, }); const ProductListSuccess = (data) => ({ type: PRODUCT_LIST_SUCCESS, data, }); const ProductListFailure = (error) => ({ type: PRODUCT_LIST_FAILURE, error, }); ``` 然後在 reducer 裡面寫: ```javascript const reducer = (state = initialState, action = {}) => { switch (action.type) { case POST_REQUEST: { const { data } = action; return { ...state, { ...state.post, isPending: true, }, }; } case POST_SUCCESS: { const { data } = action; return { ...state, { ...state.post, isPending: false, data, }, }; } case POST_FAILURE: { const { error } = action; return { ...state, { ...state.post, isPending: false, error, data: undefined, }, }; } case PRODUCT_LIST_REQUEST: { const { data } = action; return { ...state, { ...state.products, isPending: true, }, }; } case POST_LIST_SUCCESS: { const { data } = action; return { ...state, { ...state.products, isPending: false, data, }, }; } case POST_FAILURE: { const { error } = action; return { ...state, { ...state.products, isPending: false, error, data: [], }, }; } default: return state; }; } ``` 可是這樣很煩啊,一樣的東西要寫好多遍。 --- 如果加上兩個 action 來選不同的欄位,那可以簡化為: ```javascript const REQUEST = 'REQUEST'; const SUCCESS = 'SUCCESS'; const FAILURE = 'FAILURE'; const Request = (data) => ({ type: REQUEST, data }); const Success = (data) => ({ type: POST_SUCCESS, data }); const Failure = (error, data) => ({ type: POST_FAILURE, error, data }); const requestState = { isPending: false, data: undefined, error: undefined, }; const requestReducer = (state = requestState, action = {}) => { switch (action.type) { case REQUEST: { const { data } = action; return { ...state, isPending: true, data, }; } case SUCCESS: { const { data } = action; return { ...state, isPending: false, data, }; } case FAILURE: { const { data, error } = action; return { ...state, isPending: false, data, error, }; } default: return state; } } const POST = 'POST'; const PRODUCT_LIST = 'PRODUCT_LIST'; const Post = (data) => ({ type: POST, data }); const ProductList = (data) => ({ type: PRODUCT_LIST, data }); const initialState = { post: requestState, productList: { ...requestState, data: [] }, }; const reducer = (state = initialState, action = {}) => { switch (action.type) { case POST: { const { data: nextAction } = action; const post = requestReducer(state.post, nextAction); return { ...state, post, }; } case PRODUCT_LIST: { const { data: nextAction } = action; const products = requestReducer(state.post, nextAction); return { ...state, products, }; } default: return state; }; ``` 然後在你的 saga 或是 thunk action 裡面寫: ```javascript function* post() { try { yield put(Post(Request())); const { data } = await getPost(); yield put(Post(Success(data))); } catch (error) { yield put(Post(Failure(error))); } } function* products() { try { yield put(ProductList(Request([]))); const { data } = await getProducts(); yield put(ProductList(Success(data))); } catch (error) { yield put(ProductList(Failure(error, []))); } } ``` 要是再把怎麼處理資料的 action 拆出來: ```javascript const RESET = 'RESET'; const CONCAT = 'CONCAT'; const Reset = () => ({ type: RESET }); const Concat = (data) => ({ type: CONCAT, data }); const arrayReducer = (state = [], action = {}) { switch (action.type) { case RESET: { return []; } case CONCAT: { return [...state, ...action.data]; } default: return state; }; } ``` 甚至可以繼續組合下去: ```javascript function* products(reset = false) { try { let next; if (reset) next = Reset(); yield put(ProductList(Request(next))); const { data } = await getProducts(); yield put(ProductList(Success(Concat(data)))); } catch (error) { yield put(ProductList(Failure(error))); } } ``` 完。 --- 留個紀錄: <blockquote class="twitter-tweet"><p lang="en" dir="ltr">&gt; Endofunctions are monoids, pointwise monoidal operations are monoids, and combining these two function-monoids give us reducers!<br><br>👀<a href="https://t.co/BQoCc1Rmdd">https://t.co/BQoCc1Rmdd</a></p>&mdash; Some(@cλβαι) (@_cybai) <a href="https://twitter.com/_cybai/status/1230081117786525696?ref_src=twsrc%5Etfw">February 19, 2020</a></blockquote> 還是有人注意到 reducer 和 monoid 的關聯的。