Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hox v2 RFC #37

Closed
awmleer opened this issue Jun 8, 2020 · 24 comments
Closed

hox v2 RFC #37

awmleer opened this issue Jun 8, 2020 · 24 comments

Comments

@awmleer
Copy link
Member

awmleer commented Jun 8, 2020

背景

在 hox v1 中,我们的实现方案是在应用的 React 组件树之外,额外创建一个组件树,以存储 hox 中的 model。然而,这种方案渐渐显露出较多的弊端:

为了解决上述问题,在此 RFC 中,尝试将底层实现改为基于 Context。不过基于 Context 虽然可以解决上述全部问题,但也会存在一些新的弊端:

  • API 较为复杂,特别是需要用户手动在组件树中添加 Provider

API

创建 model

import {createModel} from 'hox'

function useCounter() {
  // ...
}

export const CounterModel = createModel(useCounter)
// 或
export default createModel(useCounter)

提供 model

import {CounterModel} from './counter.model.ts'

function App() {
  return (
    <CounterModel.Provider>
      <div>
        {/* ... */}
      </div>
    </CounterModel.Provider>
  )
}

获取/订阅 model

import {useModel} from 'hox'
import {CounterModel} from './counter.model.ts'

function Foo() {
  const counterModel = useModel(CounterModel)
  return (
    ...
  )
}

只读(对应 v1 API 中的 useXxxModel.data

<CounterModel.Provider ref={yourModelRef}> // 通过 ref 的方式获取
</CounterModel.Provider>

传参

<CounterModel.Provider startFrom={123}> // 通过 ref 的方式获取
</CounterModel.Provider>
interface Props {
  startFrom: number
}
const CounterModel = createModel<Props>((props) => {
  const [count, setCount] = useState(props.startFrom)
  // ...
})

由于存在参数传递,需要给 createModel 增加一个 options.memo 参数来控制何时触发重渲染:

const CounterModel = createModel<Props>((props) => {
  const [count, setCount] = useState(props.startFrom)
  // ...
}, { // <- options
  memo: true // 开启 memo 后,Provider 的重渲染逻辑和普通 React 组件被 memo 后的逻辑类似
})

如果语法较为复杂的话,可以考虑把 memo 的默认值设置为 true,因为绝大部分场景下都是需要 memo 的。

其他

是叫 model 好还是叫 store 好?

@h-a-n-a
Copy link
Contributor

h-a-n-a commented Jun 9, 2020

有点没明白这个 ref 的使用方法,不知道是不是我理解的这样:

const Foo = () => {
  const counterModelRef = useRef(null)
  return (
     <CounterModel.Provider ref={counterModelRef}> // 通过 ref 的方式获取
         {counterModelRef.current.xxx} 
     </CounterModel.Provider>
  )
}

如果是这样的话这个命名上会不会和 props.ref 冲突呢?

是不是可以搞一个 useReadonlyModel 来访问只读数据?感觉这样会精简一点

@awmleer
Copy link
Member Author

awmleer commented Jun 10, 2020

有点没明白这个 ref 的使用方法,不知道是不是我理解的这样:

const Foo = () => {
  const counterModelRef = useRef(null)
  return (
     <CounterModel.Provider ref={counterModelRef}> // 通过 ref 的方式获取
         {counterModelRef.current.xxx} 
     </CounterModel.Provider>
  )
}

如果是这样的话这个命名上会不会和 props.ref 冲突呢?

是不是可以搞一个 useReadonlyModel 来访问只读数据?感觉这样会精简一点

本来就是 forwardRef 的,何谈命名冲突呢。。

useReadonlyModel 这样的 API 可以加,但是未必有什么价值,因为相比于 ref,它毕竟是个 Hook,会受到各种限制

@lulusir
Copy link

lulusir commented Jun 15, 2020

改成这样的api和constate这些用context的库有什么区别呢

1。即便可以通过 custom renderer 解决,但是又会导致包体积较大。
---- 这里不是准备分出几个平台的包,来按需引入吗?

  1. 无法和 Context 配合使用,在某些场景下瓶颈非常明显
    ---- 是否可以提供一个createModelWithContext的方法,把context也注入到额外的组件树中;个人感觉useRequest这样的副作用不应该放在model中,model提供状态和方法就可以了

@lili21
Copy link

lili21 commented Jun 17, 2020

手动添加Provider?那对于数量不确认的叶子节点呢?

@jinxin0112
Copy link

v2 啥时候出,不能配合 context 很恼火,react-router 里的 hooks 都不能用

@tivjoe
Copy link

tivjoe commented Jul 5, 2020

这样的改动违背了最初的设计理念,不叫modal了确实叫store了,称不上是下一代的状态管理库了,和其它context的库有什么区别呢?

@X-neuron
Copy link

X-neuron commented Jul 7, 2020

从V2 整体 更新点 和 解决的问题来看
基于context 加上 provider 有点 unstate-next化了。
保持原样,要解决这些问题,又有点向 recoil靠拢了..
喜欢Hox的地方在于 直接使用Hook 处理传递数据,类似订阅的方式,同步状态..

不管v2怎么样,我觉得保持API简洁,初心不丢最主要...要不 和其他的两个相比,要差异化竞争就比较困难了...

@brickspert
Copy link
Member

看了下 unstated-next API,感觉我们就是多了一个 memo,其它的几乎一样。

我给个极简思路,我们只在 v1 基础上提供一个 Provider,如果用户使用了 Provider,我们就把 model 挂在这个 Provider 上。如果没有提供,则保持 v1 的逻辑。

@X-neuron
Copy link

看了下 unstated-next API,感觉我们就是多了一个 memo,其它的几乎一样。

我给个极简思路,我们只在 v1 基础上提供一个 Provider,如果用户使用了 Provider,我们就把 model 挂在这个 Provider 上。如果没有提供,则保持 v1 的逻辑。

保持v1的逻辑,context #20 #36 的问题 怎么解决? unstat-next的 provider后 API使用比较不雅,如果和recoil 类似,只在root根上加个provider, 其余仍保持 v1的 api 是可以接受的。

仔细和recoil使用场景对比了下,recoil能解决的问题,hox基本能解决,recoil和react原生配合的更默契些,比如和suspense的使用,API较复杂,学习成本较高。hox在逻辑复用上要更方便,API简单,学习成本较低。
对比后主要担心以下一点:
v2 不知道能不能平稳支持 Concurrent Mode 或者是 即将到来的17版的一些其他特性。

@chenhx2015
Copy link

Context方式实现的化,Provider好像必不可少啊,添加 Provider的写法很啰嗦,再想想看还有没有别的办法吧。

@chj-damon
Copy link

既然基于Context,为什么我不直接用Context,兜兜转转又回来了?

@fattypanda
Copy link

v2 看起来真的还不如 unstated-next 来的实在,还简单快捷;
可以考虑 zustand 的方式,不过 zustand 没有默认值传递,也不能和其它 hooks (比如 useRequest )一起用;
希望能参考一下其优点;

@chj-damon
Copy link

相比起redux-like的解决方案,我现在反而更喜欢jotai/recoil这种

@awmleer
Copy link
Member Author

awmleer commented May 18, 2022

如果应用的最外层加一个 <HoxRoot>...</HoxRoot>,就可以支持创建全局 store 了

@awmleer awmleer mentioned this issue May 19, 2022
@awmleer
Copy link
Member Author

awmleer commented May 19, 2022

PR 已提 #90,欢迎大家来 review~

@CJY0208
Copy link
Member

CJY0208 commented May 19, 2022

加入 Context 后,hox@v2 多了对局部共享 store 的支持,自由划定共享数据的界限,很 nice

确实会产生 Provider 嵌套问题,对此目前建议的方案为 HoxRoot,使用体验类似 v1

不过这个模式相当于强制全局共享了,也丢失了对局部状态共享的支持

另外,Provider 的嵌套衍生的问题是,共享模块之间会有依赖顺序关系的限制

<AProvider>
  <BProvider>
      只能 B 依赖 A,无法反向依赖
  </BProvider>
</AProvider>

这个依赖顺序限制与 v1 的顺序无关有所不同,虽然新的优势为可以避免依赖产生的问题,但一定程度上也降低自由度

循环依赖的问题个人理解留给用户考虑更好一些,hox 可以做一些循环更新检测的告警和阻断,但机制上允许循环依赖

结合以上和目前的 v2 文档,个人感觉有下边的这些问题

  1. HoxRoot + createGlobalStore 丢失了对局部状态共享的支持
  2. 局部 store 的嵌套问题衍生了依赖顺序问题,行为与 HoxRoot 中的 store 不一致
  3. globalStore 和局部 store 的 create 方法不一致,且方法返回不一致,有认知成本

针对这些问题,我理想中的 hox 大概是这样

import { createStore, HoxRoot, createHoxContext } from 'hox'

// 全局 store:依赖 <HoxRoot> 的创建,且有 .data 功能
const useGlobal = createStore(() => {...}) // 默认是全局
const jsx_global = (
  <HoxRoot>
    /* 内部可访问 useGlobal */
  </HoxRoot>
)

// 局部 store - 定义方式 1:依赖 <useStore.Provider> 的创建,无 .data 功能
const useScoped_1 = createStore(() => { ... }, {
  context: true // 声明为局部 store,内部自动创建上下文
})
const jsx_scoped_1 = (
  <useStore.Provider>
    /* 内部可访问 useScoped_1 */
  </useStore.Provider>
)

// 局部 store - 定义方式 2:依赖 <customContext.Provider> 的创建,无 .data 功能
const customContext = createHoxContext()
const useScoped_2 = createStore(() => { ... }, {
  context: customContext // 声明为局部 store 且指定上下文
})
const useScoped_3 = createStore(() => { ... }, {
  context: customContext // 声明为局部 store 且指定上下文
})
const jsx_scoped_2 = (
  <customContext.Provider>
    /* 内部可访问 useScoped_2、useScoped_3,且两者可以相互依赖 */
  </customContext.Provider>
)

以上设计有如下收益

  1. 解决了局部 store 的上下文嵌套问题
  2. 局部 store 间依赖顺序无关,保持与 HoxRoot 一致
  3. 特定的 hoxContext 可以作为 HoxRoot 的底层实现
  4. 版本使用方式变更不大,除了对 createModel 的重命名,仅新增了 HoxRoot, createHoxContext 与 .Provider 设计

@awmleer
Copy link
Member Author

awmleer commented May 20, 2022

@CJY0208 我来回复一下~

HoxRoot + createGlobalStore 丢失了对局部状态共享的支持

可能是我文档没写清楚,其实局部状态和全局状态并不是二选一的,用户可以同时 createGlobalStorecreateStore,当然,会有一点限制:局部状态可以依赖全局状态,但是全局状态不能依赖局部状态。

globalStore 和局部 store 的 create 方法不一致,且方法返回不一致,有认知成本

这一点我觉得有利有弊,创建 store 存在两个函数,的确有认知成本,但是带来的好处是更加明确,用户也能很清楚的分辨出哪些是全局状态哪些是局部状态,甚至在做代码搜索的时候,直接搜索 createGlobalStore 就能搜索到全部的全局状态,而如果只是通过一个参数来控制,那么就不太方便做检索了。此外,createGlobalStoreoptions 参数和 createStore 是不同的,例如 createGlobalStore 不支持手动选择是否 memo、后面可能额外支持配置 lazy 是否懒加载,这些参数差异后面可能会比较大,如果把这两个函数合并成一个,感觉未必认知成本很低。

你提到了通过手动指明 context 的方式来实现,感觉是想解决局部状态一次性声明一批的这种情况?例如下面这种:

<AStore.Provider>
  <BStore.Provider>
    <CStore.Provider>
      ...
    </CStore.Provider>
  </BStore.Provider>
</AStore.Provider>

如果用 createHoxContext 的话,就可以简写成:

const fooContext = createHoxContext()

<fooContext.Provider>
  ...
</fooContext.Provider>

对于一些复杂情况(比如复杂页面中一连串声明五六个 store,我就遇到过这种情况),这样写应该会更简单,但是如果强制每次都得先创建一个 hox context,再创建 store,可能就有些繁琐了,而且,不利于 store 的细粒度组合,举个例子:

<AStore.Provider>
  <BStore.Provider>
    ...
  </BStore.Provider>
</AStore.Provider>

<AStore.Provider>
  <CStore.Provider>
    ...
  </CStore.Provider>
</AStore.Provider>

在页面 1 中,我希望把 A 和 B 组合起来使用,在页面 2 中,我希望把 A 和 C 组合起来使用,这种情况下,AStore 的 createStore 就很难写了,因为只能指明一个 context。

我在想 StoreProvider 嵌套地狱的这种情况肯定会存在的,如果要解决的话,倒是有另外一种思路,提供一个批量 Store 的语法糖:

<BatchStoreProvider members={[AStoreProvider, BStoreProvider]}>
  ...
</BatchStoreProvider>

和预先声明 context 的思路不同,BatchStoreProvider 是在使用 Provider 的时候再做组合的,这样就提供了更好的灵活性。

不过,就像你说的,相较于预先声明 context,自然这里就没有办法自动处理 Provider 之间的依赖顺序了:

局部 store 的嵌套问题衍生了依赖顺序问题,行为与 HoxRoot 中的 store 不一致

需要让用户手动按先后顺序排放好。或者也许有办法实现自动判断依赖顺序,但是比较难实现……?

其实我觉得依赖顺序倒还好,我在实际使用过程中(因为我之前大量使用了 reto),几乎没有因为依赖顺序而花费过心思,显示的声明 Store 的顺序也让我能够更清晰的看到整个状态树的脉络,反倒挺好的。

@CJY0208
Copy link
Member

CJY0208 commented May 20, 2022

OK,createGlobalStorecreateStore 区分的意图理解了,不过 createStore 的返回值和 createGlobalStore 不同还是不太理解

赞成 <BatchStoreProvider > 做法,相对预先声明 context,这个设计的意图更清晰

这种做法应该可以实现依赖顺序无关,之前我做过类似的事情

@awmleer
Copy link
Member Author

awmleer commented May 20, 2022

createStorecreateGlobalStore 返回值不同确实有点容易让人迷惑,我在写的时候也感觉到这个问题了,不过毕竟 createGlobalStore 目前是只有一个返回值的,如果也强行包一层数组的话会不会有点奇怪……?

const [useFooStore] = createGlobalStore(...)
const [useBarStore, BarStoreProvider] = createStore(...)

@awmleer
Copy link
Member Author

awmleer commented May 20, 2022

这样设计 API 的话,倒是有个额外的好处,如果后面的版本 createGlobalStore 想增加一些额外的返回内容,就可以很方便的扩充了 🤔 这样来看的话,感觉也未尝不可

@CJY0208
Copy link
Member

CJY0208 commented May 20, 2022

依赖了数组的解构,后续拓展可能也会受数组顺序限制😂

@awmleer
Copy link
Member Author

awmleer commented May 20, 2022

2-3 个的话,用数组解构还好,多了的话就比较难受了

@awmleer
Copy link
Member Author

awmleer commented May 20, 2022

也许可以把 .data 属性改成独立的 getFooStore 函数,例如这样:

const useFooStore = createGlobalStore(...)
useFooStore.data
// ⬇️
const [useFooStore, getFooStore] = createGlobalStore(...)

这样有些好处:

  • 可以手动控制要不要暴露只读函数(其实我觉得意义也不太大?)
  • 方便查找引用
  • 更明确,不然有些 useXxxStore 可以 .data,有些不能,可能会容易让人困惑

但也有坏处:

  • 语法稍微繁琐一点

@awmleer awmleer pinned this issue May 20, 2022
@awmleer awmleer closed this as completed May 24, 2022
@awmleer
Copy link
Member Author

awmleer commented May 24, 2022

npm 包 v2.0.0-alpha.0 已发

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests