基于 react-hook 实现的 sku 选择
之前在项目中写过 sku 的选择需求;这次想用 react-hook+TypeScript 来写一下,体验一下感受
- 不同的属性选择后显示不同的价格和数量
- 当 sku 数量为 0 时,该属性不可点击
- 当只有一种 sku 有数量时怎么默认选中
- react 想较于 vue,在写完业务逻辑后更需要考虑是否有不必要的
re-render
- 正则匹配在循环中不能加 g 修饰符
服务端给我们数据基本是这样
const sku = {
'黑;16G;电信': { price: 100, count: 10 },
'黑;16G;移动': { price: 101, count: 11 },
'黑;16G;联通': { price: 102, count: 0 },
'黑;32G;电信': { price: 103, count: 13 },
'黑;32G;移动': { price: 104, count: 14 },
'黑;32G;联通': { price: 105, count: 0 },
'金;16G;电信': { price: 106, count: 16 },
'金;16G;移动': { price: 107, count: 17 },
'金;16G;联通': { price: 108, count: 18 },
'金;32G;电信': { price: 109, count: 0 },
'金;32G;移动': { price: 110, count: 20 },
'金;32G;联通': { price: 111, count: 21 },
'白;16G;电信': { price: 112, count: 0 },
'白;16G;移动': { price: 113, count: 23 },
'白;16G;联通': { price: 114, count: 24 },
'白;32G;电信': { price: 115, count: 0 },
'白;32G;移动': { price: 116, count: 26 },
'白;32G;联通': { price: 117, count: 27 }
};
const key = [
{ name: '颜色', item: ['黑', '金', '白'] },
{ name: '内存', item: ['16G', '32G'] },
{ name: '运营商', item: ['电信', '移动', '联通'] }
];
- 我会创建
skuList
,用来记录当前选中的属性值;
//属性集合
const [skuList, setSkuList] = useState<string[]>([])
- 每当用户点击是更新
skuList
const handleClick = (e: React.MouseEvent<HTMLSpanElement>, fIndex: number, sIndex: number) => {
const val = e.currentTarget.innerText;
skuList[fIndex] = val
setSkuList([...skuList])
}
- 怎么去匹配取值,我这边用正则匹配,第二次去数据缓存中取。
//tool.ts
export default function getPriceAndCount(str: string, arr: skuType, keyNum: number) {
const r = new RegExp(`^${str}$`);
let result: resultItem<number> = {} as any;
const _list: number[] = [];
return Object.keys(arr).reduce((p: resultItem<number>, c: string) => {
if (r.test(c)) {
p.count = (p.count || 0) + arr[c].count;
const _p = arr[c].price || 0;
_list.push(_p);
}
p.price = [Math.min.apply(null, _list), Math.max.apply(null, _list)];
return p;
}, result);
}
//hook.ts
//维护一份缓存的结果结合
const [cashResult, setCashResult] = useState<cashResult<number>>({} as any);
useEffect(() => {
if (skuList.length) {
let res: resultItem<number> = {} as any;
if (cashResult[skuStr]) {
res = cashResult[skuStr];
} else {
res = getPriceAndCount(skuStr, sku, key.length);
cashResult[skuStr] = res;
setCashResult({ ...cashResult });
}
setResult(res);
}
}, [skuList]);
这样基本实现了点击属性返回 sku 的结果值
- 需要一份禁用的数组 list,
const [disableList, setDisableList] = useState<number[][]>([]);
- 处理两种情况;例如我们有颜色,内存,运营商,3 个商品属性,当用户点了 2 个时,和 3 个时取计算数量为 0 的 sku,将其置为
disable
const handleDisableList = (skuList: string[], key: keyItem[]) => {
let idx = -1;
const num = skuList.filter((el, index) =>
el !== (undefined || '[^;]*') ? true : ((idx = index), false)
).length;
if (num === key.length - 1) {
key[idx].item.forEach((i: string, y: number) => {
let _pre = [...skuList];
_pre[idx] = i;
const res = getPriceAndCount(_pre.join(';'), sku, key.length);
if (res.count === 0) {
//注意初始化的位置
let initDisableList = Array.from(key, (): number[] => []);
initDisableList[idx].push(y);
setDisableList([...initDisableList]);
}
});
} else if (num === key.length) {
//注意初始化的位置
let initDisableList = Array.from(key, (): number[] => []);
key.forEach((el: keyItem, idx: number) => {
el.item.forEach((i: string, y: number) => {
let _pre = [...skuList];
_pre[idx] = i;
const res = getPriceAndCount(_pre.join(';'), sku, key.length);
if (res.count === 0) {
if (!initDisableList[idx].includes(y)) {
initDisableList[idx].push(y);
}
}
});
});
setDisableList([...initDisableList]);
}
};
useEffect(() => {
if (skuList.length && skuList.every(el => el !== '[^;]*')) {
handleDisableList(skuList, key);
}
}, [skuList]);
//template
return (
<div>
<p>{el.name}: </p>
{
el.item.map((i: string, sIndex: number) => (
<span
className={`${activeStr === i ? 'item active' : 'item'} ${disableList.includes(sIndex) ? 'disable' : ''}`}
key={i}
onClick={(e) => handlItemClick(e, fIndex, sIndex)}
>
{i}
</span>
))
}
</div>
)
- 当且仅当第一次初始化时以及最后 result 的 count 与第一次 count 不为 0 时的值一样,那就说明后面一值在加 0,这就是我们要的结果,我们只要改造一下
getPriceAndCount
函数就可以了
export default function getPriceAndCount(str: string, arr: skuType, keyNum: number) {
let isFirstInit = false;
const r = new RegExp(`^${str}$`);
let result: resultItem<number> = {} as any;
const firstData: resultItem<number> = {} as any;
const _list: number[] = [];
if (new Array(keyNum).fill('[^;]*').join(';')) {
isFirstInit = true;
}
result = Object.keys(arr).reduce((p: resultItem<number>, c: string) => {
if (r.test(c)) {
//新增
if (isFirstInit && arr[c].count) {
firstData.price = [arr[c].price];
firstData.count = arr[c].count;
firstData.skuStr = c.split(';');
}
p.count = (p.count || 0) + arr[c].count;
const _p = arr[c].price || 0;
_list.push(_p);
}
p.price = [Math.min.apply(null, _list), Math.max.apply(null, _list)];
return p;
}, {} as resultItem<number>);
//所有的结果只有一种情况有数量
if (isFirstInit && firstData.count === result.count) {
return firstData;
} else {
return result;
}
}
- 拿到结果怎么利用呢?因为我有返回
firstData.skuStr
这个就是skuList
我只要setSkuList(firstData.skuStr)
就可以了。剩下就会自动默认选中以及禁用
- 针对
skuItem
组件
const handlItemClick: handleClick = (e, fIndex, sIndex) => {
if (disableList.includes(sIndex)) return
handleClick(e, fIndex, sIndex)
}
return (
<div>
<p>{el.name}: </p>
{
el.item.map((i: string, sIndex: number) => (
<span
className={`${activeStr === i ? 'item active' : 'item'} ${disableList.includes(sIndex) ? 'disable' : ''}`}
key={i}
onClick={(e) => handlItemClick(e, fIndex, sIndex)}
>
{i}
</span>
))
}
</div>
)
可以看到我这边主要变化的activeStr
和disableList
,所以可以通过React.meno
处理避免不必要的 re-render
// memo优化策略
function areEqual(prevProps: PropsSkuItem, nextProps: PropsSkuItem) {
// console.log(nextProps.disableList)
return (
prevProps.activeStr === nextProps.activeStr &&
// prevProps.activeNum === nextProps.activeNum &&
prevProps.disableList.toString() === nextProps.disableList.toString()
)
}
- 同理
Result
组件
return (
<div>
<p>价格:
{
result.price
? (result.price[0] === result.price[1] || result.price.length === 1)
? result.price[0]
: `${result.price[0]} - ${result.price[1]}`
: ''
}
</p>
<p>数量: {result.count}</p>
</div>
)
可以看到我这边主要变化的result
// memo优化策略
function areEqual(prevProps: PropsResult, nextProps: PropsResult) {
return (
prevProps.result === nextProps.result
)
}