一些基本的配置(这里使用 toolkit)可以在这篇笔记中找到:react-redux 使用小结,这里不多赘述。
触发副作用主流的操作方式有两种:
组件内操作
适合只会在当前组件中触发的 API 操作
写一个 action creator 进行操作
适合跨组件操作
二者并没有谁好谁坏的区别,主要还是依赖于业务需求
cart slice 的实现如下:
import { createSlice } from '@reduxjs/toolkit';
import { uiActions } from './ui-slice';
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
totalQuantity: 0,
totalAmount: 0,
},
reducers: {
addItemToCart(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
total: newItem.price,
title: newItem.title,
});
return;
}
existingItem.quantity++;
existingItem.total += existingItem.price;
},
removeItemFromCart(state, action) {
state.totalQuantity--;
const id = action.payload;
const existingItem = state.items.find((item) => item.id === id);
if (existingItem.quantity === 1) {
state.items = state.items.filter((item) => item.id !== id);
return;
}
existingItem.quantity--;
existingItem.total -= existingItem.price;
},
},
});
export const cartActions = cartSlice.actions;
export default cartSlice;
这个就相当于一个比较基本的购物车功能
UIslice:
import { createSlice } from '@reduxjs/toolkit';
const uiSlice = createSlice({
name: 'ui',
initialState: {
cartIsVisible: false,
notification: null,
},
reducers: {
toggle(state) {
state.cartIsVisible = !state.cartIsVisible;
},
showNotification(state, action) {
state.notification = {
status: action.payload.status,
title: action.payload.title,
message: action.payload.message,
};
},
},
});
export const uiActions = uiSlice.actions;
export default uiSlice;
这就是一些 UI 相关的操作。
app.js:
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import Cart from './components/Cart/Cart';
import Layout from './components/Layout/Layout';
import Products from './components/Shop/Products';
import Notification from './components/UI/Notification';
import { uiActions } from './store/ui-slice';
// to avoid empty data being sent during initial mount
let isInitial = true;
function App() {
const dispatch = useDispatch();
const { cartIsVisible, notification } = useSelector((state) => state.ui);
const cart = useSelector((state) => state.cart);
useEffect(() => {
const sendCartData = async () => {
dispatch(
uiActions.showNotification({
status: 'pending',
title: 'sending',
message: 'Sending cart data',
})
);
const res = await fetch('some api here', {
method: 'PUT',
body: JSON.stringify(cart),
});
if (!res.ok) {
throw new Error('Sending cart data failed.');
}
dispatch(
uiActions.showNotification({
status: 'success',
title: 'Success...',
message: 'Sent cart data successfully.',
})
);
};
if (isInitial) {
isInitial = false;
return;
}
sendCartData().catch((error) => {
dispatch(
uiActions.showNotification({
status: 'error',
title: 'Error...',
message: 'Sending cart data failed.',
})
);
});
}, [cart, dispatch]);
return (
<>
{notification && (
<Notification
status={notification.status}
title={notification.title}
message={notification.message}
/>
)}
<Layout>
{cartIsVisible && <Cart />}
<Products />
</Layout>
</>
);
}
export default App;
实现上来说如下:
这里的一些流程:
|- useEffect
| |- sendCartData(async call)
| | |- multiple dispatches
这个就是比较直接的操作,即在 useEffect 中调用异步操作,并且在 thenable 中进行结果的处理(这里就是 redux 的触发)。
之前直接利用 redux toolkit 写的 action 如下:
这里就是在 slice 外写了一个 customer action creator,也就是一个 thunk。实现方式,就像是在 toolkit 出来之前就要手动写很多的 action 这种感觉。
thunk 的定义如下:
a function that delays an action until later
一个延迟触发 action 的函数
当然,thunk 之类的生态圈已经发展了很多年了(在 toolkit 之前就有了),比如说比较老牌的 Redux Thunk,相对而言比较新一些的 redux-saga,它们都已经在市面上运行的比较稳定,而且周下载量都很大:
![在这里插入图片描述](https://img-blog.csdnimg.cn/fc54633351ef4525afbc715d51d82df5
很多时候可以根据业务需求进行配置,比如说项目比较简单,又没有现成的脚手架,那么现成的 toolkit 的功能说不定就够了。如果业务需求比较复杂,那么可以考虑使用 thunk 或是 saga。
这里因为是对于购物车的操作,所以 custom action creator 会放在 cart slice 中:
import { createSlice } from '@reduxjs/toolkit';
import { uiActions } from './ui-slice';
const cartSlice = createSlice({
name: 'cart',
initialState: {
items: [],
totalQuantity: 0,
totalAmount: 0,
},
reducers: {
addItemToCart(state, action) {
const newItem = action.payload;
const existingItem = state.items.find((item) => item.id === newItem.id);
state.totalQuantity++;
if (!existingItem) {
state.items.push({
id: newItem.id,
price: newItem.price,
quantity: 1,
total: newItem.price,
title: newItem.title,
});
return;
}
existingItem.quantity++;
existingItem.total += existingItem.price;
},
removeItemFromCart(state, action) {
state.totalQuantity--;
const id = action.payload;
const existingItem = state.items.find((item) => item.id === id);
if (existingItem.quantity === 1) {
state.items = state.items.filter((item) => item.id !== id);
return;
}
existingItem.quantity--;
existingItem.total -= existingItem.price;
},
},
});
// custom action creator
export const sendCartData = (cart) => {
return async (dispatch) => {
dispatch(
uiActions.showNotification({
status: 'pending',
title: 'sending',
message: 'Sending cart data',
})
);
const sendRequest = async () => {
const res = await fetch('some api here', {
method: 'PUT',
body: JSON.stringify(cart),
});
if (!res.ok) {
throw new Error('Sending cart data failed.');
}
};
try {
await sendRequest();
dispatch(
uiActions.showNotification({
status: 'success',
title: 'Success...',
message: 'Sent cart data successfully.',
})
);
} catch (e) {
dispatch(
uiActions.showNotification({
status: 'error',
title: 'Error...',
message: 'Sending cart data failed.',
})
);
}
};
};
export const cartActions = cartSlice.actions;
export default cartSlice;
app.js 中的代码:
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import Cart from './components/Cart/Cart';
import Layout from './components/Layout/Layout';
import Products from './components/Shop/Products';
import Notification from './components/UI/Notification';
import { sendCartData } from './store/cart-slice';
let isInitial = true;
function App() {
const dispatch = useDispatch();
const { cartIsVisible, notification } = useSelector((state) => state.ui);
const cart = useSelector((state) => state.cart);
useEffect(() => {
if (isInitial) {
isInitial = false;
return;
}
dispatch(sendCartData(cart));
}, [cart, dispatch]);
return (
<>
{notification && (
<Notification
status={notification.status}
title={notification.title}
message={notification.message}
/>
)}
<Layout>
{cartIsVisible && <Cart />}
<Products />
</Layout>
</>
);
}
export default App;
可以看到,本质上来说,custom action creator 中的代码基本上就是将组件内部的代码移到了另一个地方进行中心化处理。这样的优点比较多,比如说 e-commerce 的项目来说,在每个商品详情页面中可以进行购物车的操作,也可以单独到购物车的页面进行操作,或是鼠标悬浮到购物车,都会弹出一个下拉框进行简单的操作等。
这样粗略一算就会有 3 个地方都会用到同样的 API 调用,同样的 UI 提示,这个情况下就可以考虑将这部分的代码封装到 custom action creator 中,减少重复代码。