在 React v18 版本中,提供了新的状态管理 Hook useSyncExternalStore。从命名上来看,意思是同步外部 Store 数据源到 React 状态。从 React 文档中,还可了解到这个 Hook 支持 Concurrent 时间分片的特性,还另外标注了一点:

The Hook is provided for library authors to integrate libraries deeply into the React model, and are not typically used in application code.

意思是这个 Hook 一般不是在业务开发内使用,主要是提供给第三方库的开发者,用于移植它们的数据源到 React state。

本文将结合几个例子,解释在 **CSR(客户端渲染)**的环境下如何使用此 Hook 同步数据。

定义外部数据源

首先,您将定义一个 createStore 函数,用于创建 Store 并导出操作状态的相关操作方法。然后使用这个函数创建 Store 并传入用于初始化的对象。

const createStore = (initialState) => {
    let currentState = initialState

    return {
        getState: () => currentState,
        setState: (newState) => (currentState = newState)
    }
}

const store = createStore({
    value1: 1,
    value2: 2
})

展示状态

接下来,您将需要创建用于展示状态的组件 BasicView,这个组件接收一个 stateKey 参数,即在状态对象中的键名。

const BasicView = ({ stateKey }) => {
    return (
        <div>
            <h2>{stateKey} : {store.getState()[stateKey]}</h2>
        </div>
    )
}

const App = () => {
    return (
        <>
            <BasicView stateKey='value1' />
            <BasicView stateKey='value2' />
        </>
    )
}

Untitled

您现在就可以在浏览器中,查看状态的内容了。

变更状态

BasicView 组件内,另外添加一个按钮元素,并绑定点击事件,在每次点击时将 stateKey 对应的状态自增 1

const BasicView = ({ stateKey }) => {
    const onAddState = () => {
        const currentState = store.getState()

        console.log('triggered', currentState[stateKey])

        currentState[stateKey] += 1

        store.setState(currentState)
    }

    return (
        <div>
            <h2>{stateKey} : {store.getState()[stateKey]}</h2>
            <button onClick={onAddState}>add {stateKey}</button>
        </div>
    )
}

Untitled

如果您点击了按钮,会发现内容毫无变化,但是 Store 里的状态对象的确发生了变化。这个原因很简单,因为您没有使用 useState 钩子 schedule 一个更新任务。

改造组件与 Store

您将需要对 BasicViewcreateStore 进行改造,并增加一个 useStore 的自定义 hook,使得 Store 内部状态发生变化时,能够自动 schedule 更新任务。

const createStore = (initialState) => {
    let currentState = initialState
    const listeners = new Set()

    return {
        getState: () => currentState,
        setState: (newState) => {
            currentState = newState

            listeners.forEach(fn => fn(newState))
        },
        subscribe: fn => {
            listeners.add(fn)

            return () => listeners.delete(fn)
        }
    }
}

const useStore = () => {
    const [state, setState] = useState(store.getState())

    useEffect(() => {
        return store.subscribe(setState)
    }, [])

    return state
}

const BasicView = ({ stateKey }) => {
    const storeState = useStore()

    const onAddState = () => {
        storeState[stateKey] += 1

        store.setState({ ...storeState })
    }

    return (
        <div>
            <h2>{stateKey} : {storeState[stateKey]}</h2>
            <button onClick={onAddState}>add {stateKey}</button>
        </div>
    )
}