useSyncState

315 阅读2分钟

在这段 React 代码中,封装 useSyncState 的目的是为了简化状态管理和同步状态更新的过程。

useSyncState 是一个自定义 Hook,它基于 React 的 useState Hook 实现。它的主要作用是封装了对状态的读取和更新,并且在更新状态时会自动同步更新一个可变的引用。这样做的目的是为了解决 React 中状态更新的异步问题,以及确保状态在组件渲染过程中的一致性。

在 React 中,使用 useState 来管理组件的状态,当调用 setState 来更新状态时,React 会对状态进行合并和批量更新,这样可能会导致在同一个渲染周期内读取到的状态并不是最新的。而 useSyncState 则通过引入一个可变的 current 引用来确保状态的同步更新,从而避免了这个问题。

举例说明,在这段代码中,有两个地方使用了 useSyncState

  1. const [syncTreeList, treeList, setTreeList] = useSyncState<insideCategoryInfoType[]>([...]:这里使用 useSyncState 来创建了 syncTreeListsetTreeList,它们是用于管理 treeList 状态的替代品。当调用 setTreeList 来更新状态时,syncTreeList 会自动更新为最新的状态。
  2. const [syncChecked, checked, setChecked] = useSyncState<categoryInfoType[]>([]):这里同样使用 useSyncState 来创建了 syncCheckedsetChecked,用于管理 checked 状态。当调用 setChecked 来更新状态时,syncChecked 也会自动更新为最新的状态。

通过封装 useSyncState,代码简化了对状态的读取和更新,同时保证了状态的同步更新,确保了状态的一致性。这样使得组件的状态管理更加可靠和高效。

import { useMemo, useState } from 'react'
import { TreeManage } from '@jz/base-providers'
import { produce } from 'immer'
import { clone, forEach, map, pullAt, remove, set, size, some, startsWith } from 'lodash-es'
import { useSyncState } from '@jz/base-react-hooks'
import { request, API } from '@/providers'
import { genTreeFromList } from '../providers'
import { categoryInfoType, insideCategoryInfoType, propsType } from '../category-selector.types'

export default function useCategory() {
    const [syncTreeList, treeList, setTreeList] = useSyncState<insideCategoryInfoType[]>([
        {
            categoryId: 'all',
            name: '全部类目'
        }
    ])
    const [loading, setLoading] = useState(true)
    const [isSearch, setIsSearch] = useState(false)
    const [searchList, setSearchList] = useState<insideCategoryInfoType[]>([])
    const [syncChecked, checked, setChecked] = useSyncState<categoryInfoType[]>([])
    const checkedKeys = useMemo(() => map(checked, o => o.categoryId), [checked])

    function $setChecked(checkedKeys: insideCategoryInfoType['categoryId'][]) {
        const treeManage = new TreeManage(syncTreeList.current, {
            fieldNames: { key: 'categoryId' }
        })
        // 根据路径进行排序,保证父级在最上面
        // ------------------------------------------------------------------------
        const checkedInfo = map(checkedKeys, k => {
            const path = treeManage.findIndexPath(k as never)!
            const node = treeManage.findNode(k as never)!
            return { path, key: k, node }
        })?.sort((a, b) => size(a.path) - size(b.path))
        // 对排序后的数组进行去重,规则: 勾选了父级则剔除子集,否则不剔除
        // ------------------------------------------------------------------------
        checkedInfo?.[0]?.key === 'all' && pullAt(checkedInfo, 0)
        const newCheckedInfo: typeof checkedInfo = []
        forEach(checkedInfo, info => {
            const hasParent = some(newCheckedInfo, newInfo => {
                return startsWith(info.path.join('-'), `${newInfo.path.join('-')}-`)
            })
            if (!hasParent) {
                newCheckedInfo.push(info)
            }
        })
        setChecked(map(newCheckedInfo, o => o.node) as categoryInfoType[])
    }

    function $remove(categoryId: insideCategoryInfoType['categoryId']) {
        const newChecked = clone(checked)
        remove(newChecked, o => o.categoryId === categoryId)
        setChecked(newChecked)
    }

    async function fetchList(params?: { parent?: number; name?: string }) {
        setIsSearch(!!params?.name)
        params?.parent || setLoading(true)
        try {
            const { data } = await request.post(`${API.ABA}/basCategory/list`, { ...params })
            if (params?.name) {
                setSearchList(data)
            } else {
                const treeManage = new TreeManage(treeList, {
                    fieldNames: {
                        key: 'categoryId'
                    }
                })
                const indexPath = treeManage.findIndexPath((params?.parent as never) || 'all')!
                setTreeList(
                    produce(draft => {
                        set(draft, `${TreeManage.indexPathToLodashPath(indexPath)}.children`, data)
                    })
                )
            }
        } finally {
            setLoading(false)
        }
    }

    async function fetchListByIds(ids: propsType['minimumCategoryIds']) {
        setLoading(true)
        try {
            const { data } = await request.post(`${API.ABA}/basCategory/listParent`, ids)
            setTreeList(
                produce(draft => {
                    draft[0].children = genTreeFromList(data)
                })
            )
        } finally {
            setLoading(false)
        }
    }

    function reset() {
        setTreeList([{ categoryId: 'all', name: '全部类目' }])
        setLoading(true)
        setIsSearch(false)
        setSearchList([])
        setChecked([])
    }

    return {
        fetchList,
        fetchListByIds,
        loading,
        treeList,
        isSearch,
        searchList,
        syncChecked,
        checked,
        checkedKeys,
        setChecked: $setChecked,
        remove: $remove,
        reset
    }
}