hox是什么
hox文档
- hox 想解决的问题,不是如何组织和操作数据,不是数据流的分层、异步、细粒度,我们希望 Hox 只聚焦于一个痛点:在多个组件间共享状态
hox实现状态共享的方式
- 首先通过自定义hook返回要共享的状态
- 全局状态:全局数组+useSyncExternalStore + 用于render后重新发布订阅状态的组件
- 局部状态:context + useSyncExternalStore + 用于render后重新发布订阅状态的组件
- hox特别新颖的点:通过渲染一个空组件,利用组件的useEffect在每次渲染后进行判断数据是否更改
基本使用
import { HoxRoot } from './hox';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<HoxRoot>
<App />
</HoxRoot>
</React.StrictMode>
);
reportWebVitals();
=============================================
import { createGlobalStore } from '../hox';
import {useState} from 'react';
function useStoreDemo(){
const [count,setCount]=useState(0);
return {
count,
setCount
}
}
export const [useCountDemo] = createGlobalStore(useStoreDemo);
==========================================
import React from 'react';
import { useEffect } from 'react';
import { useCountDemo } from './hooks/demo'
function App() {
const {count,setCount}=useCountDemo()
return (
<div className="App">
<div>
在这计数:{count}
</div>
</div>
);
}
export default App;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
-
局部状态共享
- 不同 StoreProvider 实例之间,数据是完全独立和隔离的
import { useState } from 'react'
import { createStore } from '../hox'
export const [useAppleStore, AppleStoreProvider] = createStore(() => {
const [banana, setBanana] = useState(['苹果'])
return {
banana,
setBanana
}
})
===================================
import { useEffect } from 'react';
import { useCountDemo } from '../hooks/demo'
import {useAppleStore} from '../hooks/appleDemo';
function App() {
const {count,setCount}=useCountDemo()
const {banana,setBanana} =useAppleStore()
function changeCount(){
setCount(count+1)
}
function changeApple(){
setBanana(v=>[...v,'葡萄'])
}
return (
<div className="demo1">
demo1...
<button onClick={changeApple}>水果计数</button>
<span>{banana}</span>
</div>
);
}
export default App;
==================================================
import React from 'react';
import { useEffect } from 'react';
import { useCountDemo } from './hooks/demo'
import Demo1 from './components/demo1';
import Demo2 from './components/demo2'
import {AppleStoreProvider} from './hooks/appleDemo';
function App() {
const {count,setCount}=useCountDemo()
return (
<div className="App">
<AppleStoreProvider>
<Demo1></Demo1>
</AppleStoreProvider>
<AppleStoreProvider>
<Demo2></Demo2>
</AppleStoreProvider>
<div>
在这计数:{count}
</div>
</div>
);
}
export default App;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
源码解析
index.js 入口文件
export { createStore } from './create-store'
export { createGlobalStore } from './create-global-store'
export { HoxRoot } from './hox-root'
export { withStore } from './with-store'
export type { CreateStoreOptions } from './create-store'
coantainer.tsx 管理每个hook
type Subscriber<T> = (data: T) => void
export class Container<T = unknown, P = {}> {
constructor(public hook: (props: P) => T) {}
subscribers = new Set<Subscriber<T>>()
data!: T
notify() {
for (const subscriber of this.subscribers) {
subscriber(this.data)
}
}
}
- 这里的hook就是用户自定义的hook,也就是createStore里的内容
- data 就是自定义hook返回的结果
export const [useAppleStore, AppleStoreProvider] = createStore(() => {
const [banana, setBanana] = useState(['苹果'])
return {
banana,
setBanana
}
})
全局状态共享的实现
import React, { ComponentType, FC, PropsWithChildren } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim'
let globalExecutors: ComponentType[] = []
const listeners = new Set<() => void>()
export function registerGlobalExecutor(executor: ComponentType) {
globalExecutors = [...globalExecutors, executor]
listeners.forEach(listener => listener())
}
export const HoxRoot: FC<PropsWithChildren<{}>> = props => {
const executors = useSyncExternalStore(
onStoreChange => {
listeners.add(onStoreChange)
return () => {
listeners.delete(onStoreChange)
}
},
() => {
return globalExecutors
}
)
return (
<>
{executors.map((Executor, index) => (
<Executor key={index} />
))}
{props.children}
</>
)
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
import { Container } from './container'
import { registerGlobalExecutor } from './hox-root'
import { useDataFromContainer } from './use-data-from-container'
import { DepsFn } from './types'
import { memo, useEffect, useState } from 'react'
export function createGlobalStore<T>(hook: () => T) {
let container: Container<T> | null = null
function getContainer() {
if (!container) {
throw new Error(
'Failed to retrieve data from global container. Please make sure you have rendered HoxRoot.'
)
}
return container
}
const GlobalStoreExecutor = memo(() => {
const [innerContainer] = useState(() => new Container<T>(hook))
container = innerContainer
innerContainer.data = hook()
useEffect(() => {
innerContainer.notify()
})
return null
})
registerGlobalExecutor(GlobalStoreExecutor)
function useGlobalStore(depsFn?: DepsFn<T>): T {
return useDataFromContainer(getContainer(), depsFn)
}
function getGlobalStore(): T | undefined {
return getContainer().data
}
return [useGlobalStore, getGlobalStore] as const
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
-
useDataFromContainer
- 将传入createStore的hook,通过useSyncExternalStore订阅起来
- 发生在用户使用 const {count,setCount}=useCountDemo();时
import { useRef } from 'react'
import { Container } from './container'
import { DepsFn } from './types'
import { useSyncExternalStore } from 'use-sync-external-store/shim'
export function useDataFromContainer<T, P>(
container: Container<T, P>,
depsFn?: DepsFn<T>
): T {
const depsFnRef = useRef(depsFn)
depsFnRef.current = depsFn
const depsRef = useRef<unknown[]>(depsFnRef.current?.(container.data) || [])
return useSyncExternalStore(
onStoreChange => {
function subscribe() {
if (!depsFnRef.current) {
onStoreChange()
} else {
const oldDeps = depsRef.current
const newDeps = depsFnRef.current(container.data)
if (compare(oldDeps, newDeps)) {
onStoreChange()
}
depsRef.current = newDeps
}
}
container.subscribers.add(subscribe)
return () => {
container.subscribers.delete(subscribe)
}
},
() => container.data
)
}
function compare(oldDeps: unknown[], newDeps: unknown[]) {
if (oldDeps.length !== newDeps.length) {
return true
}
for (const index in newDeps) {
if (oldDeps[index] !== newDeps[index]) {
return true
}
}
return false
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 这里总结一下流程:当用户传入的hook被createStore处理后,再调用hook对应的setState方法时,会触发重新render,又因为GlobalStoreExecutor通过registerGlobalExecutor将GlobalStoreExecutor组件渲染在了页面上
- GlobalStoreExecutor中的useEffect会在每次渲染后触发,从而触发订阅的发布流程,最终useDataFromContainer里的useSyncExternalStore会被重新触发,进行判断后,返回新的store状态
- 对于全局状态共享并没有使用context,直接通过将container保存进全局变量来实现
局部状态共享的实现
import React, {
createContext,
FC,
memo,
PropsWithChildren,
useContext,
useEffect,
useState,
} from 'react'
import { Container } from './container'
import { DepsFn } from './types'
import { useDataFromContainer } from './use-data-from-container'
export type CreateStoreOptions = {
memo?: boolean
}
const fallbackContainer = new Container<any>(() => {})
export function createStore<T, P = {}>(
hook: (props: P) => T,
options?: CreateStoreOptions
) {
const shouldMemo = options?.memo ?? true
const StoreContext = createContext<Container<T, P>>(fallbackContainer)
const IsolatorContext = createContext({})
const IsolatorOuter: FC<PropsWithChildren<{}>> = props => {
return (
<IsolatorContext.Provider value={{}}>
{props.children}
</IsolatorContext.Provider>
)
}
const IsolatorInner = memo<PropsWithChildren<{}>>(
props => {
useContext(IsolatorContext)
return <>{props.children}</>
},
() => true
)
const StoreExecutor = memo<PropsWithChildren<P>>(props => {
const { children, ...p } = props
const [container] = useState(() => new Container<T, P>(hook))
container.data = hook(p as P)
useEffect(() => {
container.notify()
})
return (
<StoreContext.Provider value={container}>
{props.children}
</StoreContext.Provider>
)
})
const StoreProvider: FC<PropsWithChildren<P>> = props => {
return (
<IsolatorOuter>
<StoreExecutor {...props}>
<IsolatorInner>{props.children}</IsolatorInner>
</StoreExecutor>
</IsolatorOuter>
)
}
function useStore(depsFn?: DepsFn<T>): T {
const container = useContext(StoreContext)
if (container === fallbackContainer) {
console.error(
"Failed to retrieve the store data from context. Seems like you didn't render a outer StoreProvider."
)
}
return useDataFromContainer(container, depsFn)
}
return [useStore, shouldMemo ? memo(StoreProvider) : StoreProvider] as const
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 总结一下局部状态共享的流程:获取最新状态的逻辑和全局状态共享一致,都是通过useEffect在setState后发布订阅,在useDataFromContainer中进行订阅。
- 区别在于:全局状态将container统一使用一个全局数组管理,局部状态使用context传递container,个人觉得这样做的原因是全局store较少通过一个数组一起管理没问题,但一个项目可能有很多局部状态,所以通过一个数组管理每次render都去遍历整个数组消耗太大