V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
jaydenWang
V2EX  ›  程序员

不够“坦诚”的 Zustand:我们是否为了函数式而函数式?

  •  
  •   jaydenWang · 2 天前 · 1941 次点击

    引言

    Zustand 是目前 React 生态中最流行的状态管理库之一。它以极简著称,也是我个人非常喜欢的库。 但在长期的使用中,我常常产生一种违和感:我们在一个名为“函数式”的库里,费力地模拟着面向对象。

    那个无处不在的 get()

    来看看经典的 Zustand 写法:

    const useStore = create((set, get) => ({
      count: 0,
      inc: () => set({ count: get().count + 1 }),
      actionB: () => {
        // 调用另一个 Action
        get().inc(); 
        // 获取当前状态
        const val = get().count;
      }
    }))
    

    仔细审视这个 get()

    1. **它就是 this**:它的作用就是访问当前实例的上下文。
    2. 它是“二等公民” :你必须显式地调用它 get(),而且它打破了 JS 引擎对 this 的自然优化。
    3. 心智负担:在写复杂逻辑时,你满屏都是 get().xxx,这并不比 this.xxx 优雅,反而增加了一层函数调用括号的视觉噪音。

    那个黑盒般的 set

    set 函数的设计初衷是好的(提供类似 setState 的原子更新),但在复杂场景下,它显得不够“坦诚”:

    1. 语义模糊set 隐藏了更新的细节。是合并?是替换?是深拷贝?你必须去查文档或看源码才能确定它是 "Auto Merging" 的。
    2. 逻辑断层:当你想复用一段逻辑(比如 Private Method )时,你发现你很难在 create 的闭包里优雅地定义私有辅助函数,往往只能写在外面,破坏了 Store 的内聚性。

    为了函数式而函数式?

    我们推崇函数式编程( FP ),是因为它有 纯函数无副作用引用透明 等数学上的美感。

    但 Zustand 的 Store 定义是纯函数吗?显然不是。它是一个包含了状态( State )和行为( Action )的容器。 在计算机科学中,状态 + 行为 = 对象( Object )

    既然我们本质上是在构建一个对象,为什么要回避 JS 语言原生提供的、经过几十年打磨的构建对象的最佳工具——Class

    我们为了避嫌 "OOP",发明了一套 (set, get) => ({...}) 的 DSL 。这不仅牺牲了 Class 的继承、属性访问器( Getter/Setter )等高级能力,还增加了一层理解成本。

    这是否是一种形式上的函数式正确,而非工程上的务实选择

    另一种可能性

    如果在 React 状态管理中,我们不再视 class 为洪水猛兽,而是承认它作为 "Model" 载体的合理性,会发生什么? 这或许值得我们深思。

    第 1 条附言  ·  1 天前
    React 缺失的“M”层:我开发了 Zenith ,重塑完整的 Model <https://ex.noerr.eu.org/t/1180806/info>
    18 条回复    2025-12-24 10:45:24 +08:00
    tangdw
        1
    tangdw  
       2 天前   ❤️ 1
    set 支持函数式更新 set(state => result) 不必在 set 里面调 get
    action 里面最好在顶部声明 const { count } = get() ,要是异步里面的话就只能 get().count 这也是为了避免闭包拿到旧的状态
    jaydenWang
        2
    jaydenWang  
    OP
       2 天前
    确实可以,但需要这么多"约定"和"注意事项"来规避闭包陷阱时,是否说明这套 DSL 本身在表达"有状态对象"时不够直观
    Al0rid4l
        3
    Al0rid4l  
       2 天前   ❤️ 2
    就像这个回答说的:
    「前端如今不提倡使用 class 的观点,最终都可以归结于一点:我们讨厌 this 」
    https://www.zhihu.com/question/516551830/answer/1904509977818825306
    https://www.zhihu.com/question/1951689499047334694/answer/1955619666190922360
    wakarimasen
        4
    wakarimasen  
       2 天前 via Android
    因为我们前端程序员都是共运,阶级( class )是要坚决消除的
    LaTero
        5
    LaTero  
       2 天前 via Android
    因为 this 不好用,隐式 this 尤甚,比如 C++现在就在搞显式 this 。this 和继承,几乎在每个 OOP 语言都是深坑。我不是专业写 JS 的,所以至今都搞不明白 JS 的 this 和继承……比如箭头函数好像 this 有什么特殊行为,继承又是什么原型链,总之特别复杂,毫无必要的复杂,最好的办法就是不碰它。
    codehz
        6
    codehz  
       2 天前
    我翻遍了资料也没看 zustand 说它是函数式啊?能不能先别立一个稻草人呢
    jaydenWang
        7
    jaydenWang  
    OP
       2 天前
    @codehz Zustand 的设计和使用方式,被默认理解和实践为一种‘函数式姿态’,但它在工程语义上其实是在模拟对象模型
    codehz
        8
    codehz  
       1 天前
    zustand 里唯一看上去是函数式相关的,大概只有不可变数据和纯函数更新这两点了,但完全不能和真正意义上的函数式打等号,一些资料里这样写单纯是因为他们没搞清楚概念,真正重要的是那个 flux 模型,它看似与函数式紧密联系,但实际上是完全不同层次的抽象,只能说有一定的相似性

    Flux 的核心原则包括:
    单向数据流:数据流动是单向的,避免双向绑定带来的复杂性和不可预测性。典型流程是:View 触发 Action → Dispatcher 分发 → Store 更新状态 → View 重新渲染。
    单一真相来源:应用状态集中在 Store 中,而不是散布在各个组件。
    动作驱动更新:状态变化通过明确的 Action 来驱动,便于追踪和调试。

    zustand 只是在这个基础上把开发体验做了一定的提升,简化了使用:
    不需要 Dispatcher 或严格的 Action/Reducer 分离。
    直接在 store 中定义状态和更新函数(这些函数类似于 Action Creators ),通过 set 函数不可变地更新状态。

    至于为啥不用 class ,还不是因为 js 本身局限性,无法高效跟踪深度嵌套类型的变动,只能采取创建新对象的方式来触发更新——例如 zustand 同一家出的那个 valtio ,就是使用 proxy 做的状态跟踪,为了解决 js 的局限性,其实不仅带来了很多损耗,也需要遵循很多规则( this 使用规则,还有 proxyMap proxySet 等原生对象包装)写才不会出事
    jaydenWang
        9
    jaydenWang  
    OP
       1 天前 via iPhone
    @codehz 我很认同你说的 flux 核心原则,但是 zustand 做得并不好,派生状态目前只能通过 usememo 散落在各个组件里,并没有内聚到 store 中。另外一个就是组件可以直接 set zustand 的 state ,并没有限制到只能调用 store 的 action ,在实际开发中就会出现状态和 action 可能都大量散落在组件中的情况
    jaydenWang
        10
    jaydenWang  
    OP
       1 天前 via iPhone
    @codehz zustand 的使用体验,api 设计的非常棒。我觉得它缺失的是:set 不是私有方法,没能限制在 store 中调用;派生状态没有内聚在 store 中
    codehz
        11
    codehz  
       1 天前 via Android
    @jaydenWang zustand 的派生不是直接在 selector 里写的吗,我倒好奇你是怎么用的
    语法上确实没阻止你直接导出 set 但一般人也不会这么写啊
    codehz
        12
    codehz  
       1 天前 via Android
    主要是就算你想在外面直接调用 set 也可以使用那个 selector 函数的 setState 方法,所以我完全不知道为啥你有导出 set 的想法()
    codehz
        13
    codehz  
       1 天前 via Android
    zustand 的核心就是那个 selector 机制,避免了 context 的牵一发动全身问题
    其他的一切都只是为了一个简单好用的 api ,包括那个 set get 的设计,做成这样是为了能方便被中间件扩展,某种意义上说,相比于函数式或者面向对象,zustand 的 middleware 设计更偏向于 aop 也就是面向切面编程
    netabare
        14
    netabare  
       1 天前 via iPhone
    AI ?怪不得那么喜欢 OOP 。

    有没有一种可能,你说的「 class 继承」这种「高级」功能恰恰就是 OOP 为什么坏的原因。

    那个 inc 确实丑,但我不认为这个模式有什么问题,这就是最平凡的 lambda 。「 JS 十几年打磨的 this 和 class 」,我愿意称这是今天看到的最好笑的笑话。

    至于「 class 作为 model 载体」,这句话对我来说就是一个危险信号:不再在乎「 model 如何参与渲染循环,而开始把它像业务、intelligence 一样当一个大垃圾堆,像后端人那样把一切看不懂的或者不想理解的东西塞进去」,而这就是 what class supposed to do 。

    反过来看这个「没有 this 的坏代码」:

    命名明确:count 、inc 一目了然
    显示调用:我调用 set/get 的时候我会被迫知道我在干嘛

    以及 OOP 不是只有 Jaba ,actor model 也是 OOP 。
    jaydenWang
        15
    jaydenWang  
    OP
       1 天前 via iPhone
    @netabare 五个人协同开发,每个人都知道 get,set 在干嘛,但无法避免状态和逻辑散落在组件中,彼此就无法感知其他人做了什么。这在工程化上会是一个挑战
    linkopeneyes
        16
    linkopeneyes  
       1 天前
    你不说我都快忘了还有个 get ,react 写多的人看到 set 都会想到 set(state => result) ,而且我不会在状态管理里写逻辑,他就应该纯一点 action 就是存个数据,不过他们同一家的 jotai 我觉得设计的更合理一点
    XTTX
        17
    XTTX  
       1 天前
    @netabare @codehz op 写这种帖子就是为了推自己写的状态管理库,这个 op 非常不诚实。 实际上,zustand 推荐用 selector+hooks.
    Ketteiron
        18
    Ketteiron  
       1 天前   ❤️ 1
    函数式的重点在于不可变更新。

    > 费力地模拟着面向对象
    不费劲,闭包是穷人的对象。

    你说对象=状态+行为

    更准确地说
    对象=原型链+状态+行为
    闭包=状态+行为

    get() 和 this 有本质上的区别,this 只会带来 bug 和心智负担。

    > 它是“二等公民” :你必须显式地调用它 get(),而且它打破了 JS 引擎对 this 的自然优化。
    这是我 2025 年听到最好笑的笑话,比这个还好笑 /t/1177228
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   3063 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 12:07 · PVG 20:07 · LAX 04:07 · JFK 07:07
    ♥ Do have faith in what you're doing.