предисловие
Эта статья начинается с основных требований к корзине покупок и шаг за шагом ведет вас к глубокому пониманию ям и оптимизаций в React Hook.
Из этой статьи вы можете узнать:
✨Написание React Hook + TypeScript业务组件упражняться
✨ Как использовать React.memo优化性能
✨ Как избежать крючков闭包陷阱
✨ Как аннотация что-то простое и легко в использовании自定义hook
адрес предварительного просмотра
sl1673495.github.io/react-cart
репозиторий кода
Код, используемый в этой статье, был отсортирован в репозитории github, а пример проекта создан с помощью cra.Что касается части оптимизации производительности, вы можете открыть консоль, чтобы просмотреть ситуацию повторного рендеринга.
GitHub.com/forget 1673495/день…
декомпозиция спроса
Как спрос на корзину для покупок, он должен включать несколько точек спроса:
- Отметить, Отметить все и Снять отметку.
- Рассчитать общую стоимость на основе выбранных элементов.
Реализация спроса
получить данные
Во-первых, мы запрашиваем данные корзины, что не является предметом этой статьи, это может быть реализовано через кастомный хук запроса, или это может быть реализовано с помощью обычного useState + useEffect.
const getCart = () => {
return axios('/api/cart')
}
const {
// 购物车数据
cartData,
// 重新请求数据的方法
refresh,
} = useRequest < CartResponse > getCart
Проверить реализацию логики
Мы рассматриваем объект как таблицу отображения,checkedMapЭта переменная для записи всех проверенных идентификаторов продуктов:
type CheckedMap = {
[id: number]: boolean,
}
// 商品勾选
const [checkedMap, setCheckedMap] = useState < CheckedMap > {}
const onCheckedChange: OnCheckedChange = (cartItem, checked) => {
const { id } = cartItem
const newCheckedMap = Object.assign({}, checkedMap, {
[id]: checked,
})
setCheckedMap(newCheckedMap)
}
Рассчитать общую стоимость
Затем используйте сокращение для реализации функции, которая вычисляет сумму цен.
// cartItems的积分总和
const sumPrice = (cartItems: CartItem[]) => {
return cartItems.reduce((sum, cur) => sum + cur.price, 0)
}
Тогда вам нужна функция, которая отфильтровывает все выбранные элементы
// 返回已选中的所有cartItems
const filterChecked = () => {
return (
Object.entries(checkedMap)
// 通过这个filter 筛选出所有checked状态为true的项
.filter((entries) => Boolean(entries[1]))
// 再从cartData中根据id来map出选中列表
.map(([checkedId]) => cartData.find(({ id }) => id === Number(checkedId)))
)
}
Наконец, объединив эти две функции вместе, выходит цена:
// 计算礼享积分
const calcPrice = () => {
return sumPrice(filterChecked())
}
Некоторые могут задаться вопросом, зачем из простой логики извлекать такое количество функций, здесь я объясню, дабы обеспечить читабельность статьи, я упростил реальные требования.
При реальном спросе общая цена различных видов товаров может рассчитываться отдельно, поэтомуfilterCheckedЭта функция незаменима, filterChecked может передать дополнительный параметр фильтра, чтобы вернуть выбранный элемент.子集, и здесь повторяться не будем.
выбрать всю обратную логику выбора
имеютfilterCheckedПосле функции мы также можем легко вычислить производное состояниеcheckedAll, выбрать ли все:
// 全选
const checkedAll =
cartData.length !== 0 && filterChecked().length === cartData.length
Напишите функции, чтобы выбрать все и отменить выбор всего:
const onCheckedAllChange = (newCheckedAll) => {
// 构造新的勾选map
let newCheckedMap: CheckedMap = {}
// 全选
if (newCheckedAll) {
cartData.forEach((cartItem) => {
newCheckedMap[cartItem.id] = true
})
}
// 取消全选的话 直接把map赋值为空对象
setCheckedMap(newCheckedMap)
}
если
-
全选помещатьcheckedMapКаждому идентификатору элемента присваивается значение true. -
反选помещатьcheckedMapНазначить пустой объект.
Отрисовка подкомпонента элемента
{
cartData.map((cartItem) => {
const { id } = cartItem
const checked = checkedMap[id]
return (
<ItemCard
key={id}
cartItem={cartItem}
checked={checked}
onCheckedChange={onCheckedChange}
/>
)
})
}
Видно, что логика проверять или нет легко передается дочерним компонентам.
Оптимизация производительности React.memo
На этом этапе основные требования к корзине выполнены.
Но теперь у нас новая проблема.
Это недостаток React, который по умолчанию почти не имеет оптимизаций производительности.
Давайте посмотрим на демонстрацию анимации:
На данный момент в корзине покупок 5 товаров.Глядя на распечатку консоли, каждый раз, когда она увеличивается кратно 5. Каждый раз, когда флажок установлен, он вызывает повторную визуализацию всех дочерних компонентов.
Если у нас есть 50 товаров в корзине, и мы меняемcheckedstate, что также приводит к повторному рендерингу 50 дочерних компонентов.
Мы подумали об API:React.memo, этот API в основном эквивалентен компоненту класса вshouldComponentUpdate, что, если мы используем этот API для повторного рендеринга дочерних компонентов только при проверенных изменениях?
Хорошо, приступим к написанию подкомпонента:
// memo优化策略
function areEqual(prevProps: Props, nextProps: Props) {
return prevProps.checked === nextProps.checked
}
const ItemCard: FC<Props> = React.memo((props) => {
const { checked, onCheckedChange } = props
return (
<div>
<checkbox
value={checked}
onChange={(value) => onCheckedChange(cartItem, value)}
/>
<span>商品</span>
</div>
)
}, areEqual)
В соответствии с этой стратегией оптимизации мы думаем, что до двух раз до и после рендеринга входящих реквизитовcheckedравны, то не выполняйте повторный рендеринг дочернего компонента.
Ошибки, вызванные устаревшими значениями в React Hook
Это сделано здесь? На самом деле, здесь есть ошибка.
Давайте посмотрим на уменьшение количества ошибок:
Если мы сначала щелкнем проверку первого продукта, а затем щелкнем проверку второго продукта, вы обнаружите, что статус проверки первого продукта исчез.
После проверки первого пункта наш последнийcheckedMapФактически
{ 1: true }
И из-за нашей стратегии оптимизации второй элемент не перерисовывается после проверки первого элемента,
Обратите внимание, что функциональные компоненты React повторно выполняются каждый раз при их рендеринге, что приводит к закрытию среды.
Итак, второй элемент получилonCheckedChangeОн все еще находится в закрытии функции предыдущего рендеринга компонента корзины, затемcheckedMapЕстественно, это также начальный пустой объект в последнем закрытии функции.
const onCheckedChange: OnCheckedChange = (cartItem, checked) => {
const { id } = cartItem
// 注意,这里的checkedMap还是最初的空对象!!
const newCheckedMap = Object.assign({}, checkedMap, {
[id]: checked,
})
setCheckedMap(newCheckedMap)
}
Следовательно, после проверки второго элемента правильный не рассчитывается, как ожидалось.checkedMap
{
1: true,
2: true
}
но посчитал неправильно
{ 2: true }
Это приводит к потере проверенного состояния первого элемента.
Это также проблема с общеизвестно устаревшим значением, которое приходит с замыканиями React Hook.
Тогда в это время есть простое решение в родительском компоненте сReact.useRefПередайте функцию дочернему компоненту по ссылке.
из-заrefЗа весь жизненный цикл React-компонента существует только одна ссылка, поэтому к последнему значению функции в ссылке всегда можно получить доступ через текущий, и не будет проблемы устаревших значений замыканий.
// 要把ref传给子组件 这样才能保证子组件能在不重新渲染的情况下拿到最新的函数引用
const onCheckedChangeRef = React.useRef(onCheckedChange)
// 注意要在每次渲染后把ref中的引用指向当次渲染中最新的函数。
useEffect(() => {
onCheckedChangeRef.current = onCheckedChange
})
return (
<ItemCard
key={id}
cartItem={cartItem}
checked={checked}
+ onCheckedChangeRef={onCheckedChangeRef}
/>
)
Подсборка
// memo优化策略
function areEqual(prevProps: Props, nextProps: Props) {
return prevProps.checked === nextProps.checked
}
const ItemCard: FC<Props> = React.memo((props) => {
const { checked, onCheckedChangeRef } = props
return (
<div>
<checkbox
value={checked}
onChange={(value) => onCheckedChangeRef.current(cartItem, value)}
/>
<span>商品</span>
</div>
)
}, areEqual)
На этом наша простая оптимизация производительности завершена.
useChecked для пользовательских хуков
Затем, в следующем сценарии, мы сталкиваемся с таким требованием выбрать все и снова отменить выбор. Должны ли мы повторять это снова? Это неприемлемо, мы абстрагируем эти данные и поведение с помощью пользовательских хуков.
И на этот раз мы избегаем ловушки закрытия старых значений с помощью useReducer (диспетчер сохраняет уникальную ссылку на время жизни компонента и всегда работает с последним значением).
import { useReducer, useEffect, useCallback } from 'react'
interface Option {
/** 用来在map中记录勾选状态的key 一般取id */
key?: string
}
type CheckedMap = {
[key: string]: boolean
}
const CHECKED_CHANGE = 'CHECKED_CHANGE'
const CHECKED_ALL_CHANGE = 'CHECKED_ALL_CHANGE'
const SET_CHECKED_MAP = 'SET_CHECKED_MAP'
type CheckedChange<T> = {
type: typeof CHECKED_CHANGE
payload: {
dataItem: T
checked: boolean
}
}
type CheckedAllChange = {
type: typeof CHECKED_ALL_CHANGE
payload: boolean
}
type SetCheckedMap = {
type: typeof SET_CHECKED_MAP
payload: CheckedMap
}
type Action<T> = CheckedChange<T> | CheckedAllChange | SetCheckedMap
export type OnCheckedChange<T> = (item: T, checked: boolean) => any
/**
* 提供勾选、全选、反选等功能
* 提供筛选勾选中的数据的函数
* 在数据更新的时候自动剔除陈旧项
*/
export const useChecked = <T extends Record<string, any>>(
dataSource: T[],
{ key = 'id' }: Option = {}
) => {
const [checkedMap, dispatch] = useReducer(
(checkedMapParam: CheckedMap, action: Action<T>) => {
switch (action.type) {
case CHECKED_CHANGE: {
const { payload } = action
const { dataItem, checked } = payload
const { [key]: id } = dataItem
return {
...checkedMapParam,
[id]: checked,
}
}
case CHECKED_ALL_CHANGE: {
const { payload: newCheckedAll } = action
const newCheckedMap: CheckedMap = {}
// 全选
if (newCheckedAll) {
dataSource.forEach((dataItem) => {
newCheckedMap[dataItem.id] = true
})
}
return newCheckedMap
}
case SET_CHECKED_MAP: {
return action.payload
}
default:
return checkedMapParam
}
},
{}
)
/** 勾选状态变更 */
const onCheckedChange: OnCheckedChange<T> = useCallback(
(dataItem, checked) => {
dispatch({
type: CHECKED_CHANGE,
payload: {
dataItem,
checked,
},
})
},
[]
)
type FilterCheckedFunc = (item: T) => boolean
/** 筛选出勾选项 可以传入filter函数继续筛选 */
const filterChecked = useCallback(
(func: FilterCheckedFunc = () => true) => {
return (
Object.entries(checkedMap)
.filter((entries) => Boolean(entries[1]))
.map(([checkedId]) =>
dataSource.find(({ [key]: id }) => id === Number(checkedId))
)
// 有可能勾选了以后直接删除 此时id虽然在checkedMap里 但是dataSource里已经没有这个数据了
// 先把空项过滤掉 保证外部传入的func拿到的不为undefined
.filter(Boolean)
.filter(func)
)
},
[checkedMap, dataSource, key]
)
/** 是否全选状态 */
const checkedAll =
dataSource.length !== 0 && filterChecked().length === dataSource.length
/** 全选反选函数 */
const onCheckedAllChange = (newCheckedAll: boolean) => {
dispatch({
type: CHECKED_ALL_CHANGE,
payload: newCheckedAll,
})
}
// 数据更新的时候 如果勾选中的数据已经不在数据内了 就删除掉
useEffect(() => {
filterChecked().forEach((checkedItem) => {
let changed = false
if (!dataSource.find((dataItem) => checkedItem.id === dataItem.id)) {
delete checkedMap[checkedItem.id]
changed = true
}
if (changed) {
dispatch({
type: SET_CHECKED_MAP,
payload: Object.assign({}, checkedMap),
})
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSource])
return {
checkedMap,
dispatch,
onCheckedChange,
filterChecked,
onCheckedAllChange,
checkedAll,
}
}
В настоящее время использовать его в компоненте очень просто:
const {
checkedAll,
checkedMap,
onCheckedAllChange,
onCheckedChange,
filterChecked,
} = useChecked(cartData)
Мы сделали всю сложную бизнес-логику в пользовательском хуке, включая удаление недействительного идентификатора после обновления данных и т. д. Идите и расскажите об этом своим товарищам по команде, и пусть они уйдут с работы пораньше.
useMap для пользовательских хуков
Однажды возник внезапный спрос. Нам нужно было использовать карту для записи других вещей в соответствии с идентификатором товара в корзине. Мы вдруг обнаружили, что указанный выше пользовательский хук также упаковал логику обработки карты и так далее. , мы можно только установить значение карты наtrue / false, недостаточно гибкий.
Мы далее ставимuseMapтакже вытянуть, и пустьuseCheckedMapРазработано поверх него.
useMap
import { useReducer, useEffect, useCallback } from 'react'
export interface Option {
/** 用来在map中作为key 一般取id */
key?: string
}
export type MapType = {
[key: string]: any
}
export const CHANGE = 'CHANGE'
export const CHANGE_ALL = 'CHANGE_ALL'
export const SET_MAP = 'SET_MAP'
export type Change<T> = {
type: typeof CHANGE
payload: {
dataItem: T
value: any
}
}
export type ChangeAll = {
type: typeof CHANGE_ALL
payload: any
}
export type SetCheckedMap = {
type: typeof SET_MAP
payload: MapType
}
export type Action<T> = Change<T> | ChangeAll | SetCheckedMap
export type OnValueChange<T> = (item: T, value: any) => any
/**
* 提供map操作的功能
* 在数据更新的时候自动剔除陈旧项
*/
export const useMap = <T extends Record<string, any>>(
dataSource: T[],
{ key = 'id' }: Option = {}
) => {
const [map, dispatch] = useReducer(
(checkedMapParam: MapType, action: Action<T>) => {
switch (action.type) {
// 单值改变
case CHANGE: {
const { payload } = action
const { dataItem, value } = payload
const { [key]: id } = dataItem
return {
...checkedMapParam,
[id]: value,
}
}
// 所有值改变
case CHANGE_ALL: {
const { payload } = action
const newMap: MapType = {}
dataSource.forEach((dataItem) => {
newMap[dataItem[key]] = payload
})
return newMap
}
// 完全替换map
case SET_MAP: {
return action.payload
}
default:
return checkedMapParam
}
},
{}
)
/** map某项的值变更 */
const onMapValueChange: OnValueChange<T> = useCallback((dataItem, value) => {
dispatch({
type: CHANGE,
payload: {
dataItem,
value,
},
})
}, [])
// 数据更新的时候 如果map中的数据已经不在dataSource内了 就删除掉
useEffect(() => {
dataSource.forEach((checkedItem) => {
let changed = false
if (
// map中包含此项
// 并且数据源中找不到此项了
checkedItem[key] in map &&
!dataSource.find((dataItem) => checkedItem[key] === dataItem[key])
) {
delete map[checkedItem[key]]
changed = true
}
if (changed) {
dispatch({
type: SET_MAP,
payload: Object.assign({}, map),
})
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSource])
return {
map,
dispatch,
onMapValueChange,
}
}
Это настраиваемый хук для общей операции карты, который учитывает ловушку закрытия, принимая во внимание удаление старых значений.
Вдобавок к этому мы реализуем вышеуказанноеuseChecked
useChecked
import { useCallback } from 'react'
import { useMap, CHANGE_ALL, Option } from './use-map'
type CheckedMap = {
[key: string]: boolean;
}
export type OnCheckedChange<T> = (item: T, checked: boolean) => any
/**
* 提供勾选、全选、反选等功能
* 提供筛选勾选中的数据的函数
* 在数据更新的时候自动剔除陈旧项
*/
export const useChecked = <T extends Record<string, any>>(
dataSource: T[],
option: Option = {}
) => {
const { map: checkedMap, onMapValueChange, dispatch } = useMap(
dataSource,
option
)
const { key = 'id' } = option
/** 勾选状态变更 */
const onCheckedChange: OnCheckedChange<T> = useCallback(
(dataItem, checked) => {
onMapValueChange(dataItem, checked)
},
[onMapValueChange]
)
type FilterCheckedFunc = (item: T) => boolean
/** 筛选出勾选项 可以传入filter函数继续筛选 */
const filterChecked = useCallback(
(func?: FilterCheckedFunc) => {
const checkedDataSource = dataSource.filter(item =>
Boolean(checkedMap[item[key]])
)
return func ? checkedDataSource.filter(func) : checkedDataSource
},
[checkedMap, dataSource, key]
)
/** 是否全选状态 */
const checkedAll =
dataSource.length !== 0 && filterChecked().length === dataSource.length
/** 全选反选函数 */
const onCheckedAllChange = (newCheckedAll: boolean) => {
// 全选
const payload = !!newCheckedAll
dispatch({
type: CHANGE_ALL,
payload,
})
}
return {
checkedMap: checkedMap as CheckedMap,
dispatch,
onCheckedChange,
filterChecked,
onCheckedAllChange,
checkedAll,
}
}
Суммировать
В этой статье, используя реальный спрос на корзину, мы шаг за шагом завершим оптимизацию и шагнем в яму, В этом процессе мы должны лучше понять преимущества и недостатки React Hook.
После использования пользовательских перехватчиков для извлечения общей логики объем кода в наших бизнес-компонентах значительно сокращается, и можно повторно использовать другие подобные сценарии.
React Hook предлагает новый режим разработки, но также содержит некоторые подводные камни, это палка о двух концах, и если вы используете ее правильно, она даст вам много сил.
Спасибо за прочтение, надеюсь, эта статья вдохновит вас.