当前位置: 首页 > news >正文

Biscuit:现代Web应用的状态管理框架,实现类型安全与可组合性

1. 项目概述:一个为现代Web应用打造的“饼干”框架

最近在梳理手头的几个前端项目,发现一个挺有意思的现象:很多应用的核心逻辑,其实都围绕着“状态管理”和“数据流”在打转。无论是用户登录后的身份信息、一个复杂表单的填写进度,还是一个需要跨组件共享的全局配置,本质上都是在处理数据的存储、同步与流转。传统的方案,比如直接使用React Context、Redux或者Zustand,各有各的适用场景,但也各有各的“痛点”——Context在深层嵌套时性能堪忧,Redux的样板代码让人望而生畏,而Zustand虽然轻量,但在处理复杂、结构化的状态时,有时又觉得少了点“规矩”。

就在这个当口,我注意到了tomlin7/biscuit这个项目。初看名字,你可能会会心一笑——“饼干”?这跟状态管理有什么关系?但深入了解后,你会发现,这个名字起得相当贴切。Biscuit 的设计哲学,就是希望为你的应用状态提供一种像“饼干”一样,既独立封装、便于携带(模块化),又能在需要时轻松组合、分享(可组合性)的解决方案。它不是一个试图取代一切的庞然大物,而是一个专注于解决“结构化状态”与“副作用管理”的轻量级库,尤其适合那些对代码组织、类型安全和开发体验有较高要求的现代Web应用开发者。

简单来说,Biscuit 是一个用于构建可预测、类型安全且易于测试的应用状态的JavaScript/TypeScript库。它借鉴了Elm和Redux等架构的思想,但通过更符合现代前端开发习惯的API和强大的TypeScript支持,大幅降低了使用门槛。如果你正在为一个中型以上的React、Vue或纯JS项目寻找一个清晰、健壮的状态管理方案,或者你对现有方案中繁琐的Action定义、Reducer编写感到厌倦,那么Biscuit很可能就是你一直在找的那块“小饼干”。

2. 核心设计理念与架构拆解

2.1 状态即服务:从“混乱的全局变量”到“清晰的服务单元”

在深入代码之前,理解Biscuit的核心设计理念至关重要。它彻底摒弃了将状态视为一堆散落在各处的“全局变量”的传统思维,而是倡导“状态即服务”(State as a Service)的思想。

什么是“状态即服务”?想象一下,你的应用中有用户资料、购物车、主题设置等多个功能模块。在Biscuit看来,每个模块的状态(包括数据、以及修改这些数据的方法)都应该被封装成一个独立的、自包含的“服务”。这个服务对外提供清晰的接口(读和写),内部则管理着自己的生命周期和数据逻辑。服务之间可以通过明确定义的依赖关系进行通信,而不是直接互相修改对方的内存数据。

这种设计带来了几个显著优势:

  1. 高内聚,低耦合:与用户资料相关的所有逻辑都集中在UserService里,与购物车相关的逻辑都在CartService里。修改一个服务,不会意外影响到其他服务。
  2. 极强的可测试性:每个服务都是独立的单元,你可以轻松地为其编写单元测试,模拟它的依赖,而不需要启动整个应用。
  3. 清晰的架构图:你的应用架构会变得一目了然,就是由若干个服务以及它们之间的依赖关系图构成的。这对于团队协作和新成员上手非常有帮助。

Biscuit通过createStore函数来创建这样一个服务(它称之为“Store”,但更接近“服务”的概念)。一个Store的定义包含了初始状态(initialState)、用于更新状态的纯函数reducer、以及可选的副作用处理effects

2.2 类型安全是第一要务:与TypeScript的深度集成

如果你使用TypeScript,Biscuit的魅力会加倍。它从设计之初就将类型安全放在了核心位置。你的Store状态、Reducer接受的Action、Effect的输入输出,全部都可以获得完美的类型推断和检查。

这意味着什么?意味着你在编写代码时,编辑器会给你精确的自动补全;意味着你在dispatch一个action时,如果传错了参数类型,TypeScript编译器会在你保存代码的那一刻就报错,而不是等到运行时才出现诡异的bug;意味着重构变得异常轻松和安全,因为类型系统会帮你追踪所有受影响的地方。

例如,当你定义一个增加商品数量的Action时,Biscuit能确保你传递的payload一定是一个number,并且这个Action只会被对应的Reducer处理。这种编译时的安全保障,对于构建大型、长期维护的项目来说,价值无法估量。

2.3 可组合的副作用模型:告别“Action Creator地狱”

副作用(Side Effects)是状态管理中最棘手的部分之一,比如发起一个网络请求、操作本地存储、或设置一个定时器。在Redux中,我们通常需要借助像redux-thunkredux-sagaredux-observable这样的中间件,这增加了架构的复杂性。

Biscuit采用了不同的思路。它将副作用直接集成到了Store的定义中,作为effects的一部分。一个Effect本质上是一个函数,它接收最新的状态和一些工具方法(如dispatch),然后可以执行异步操作,并在操作完成后通过dispatch派发一个同步的Action来更新状态。

这种做法的好处是,副作用逻辑与状态逻辑在定义时就被紧密地组织在了一起,而不是分散在Action Creators、Sagas和Reducers等多个文件中。更重要的是,Biscuit的Effect模型天然支持组合。你可以轻松地在一个Effect中调用另一个Effect,或者等待多个异步操作并行完成,代码逻辑清晰直观,避免了层层回调或复杂的Generator函数。

3. 从零开始:创建你的第一个Biscuit Store

理论说得再多,不如动手实践。让我们从一个最简单的计数器例子开始,感受一下Biscuit的用法。

3.1 安装与基础Store定义

首先,通过npm或yarn安装Biscuit:

npm install biscuit-store # 或 yarn add biscuit-store

假设我们有一个计数器应用,状态很简单,就是一个数字count。我们用Biscuit来管理它。

// counterStore.ts import { createStore } from 'biscuit-store'; // 1. 定义状态的类型接口 interface CounterState { count: number; } // 2. 定义初始状态 const initialState: CounterState = { count: 0, }; // 3. 创建Store const counterStore = createStore<CounterState>({ name: 'counter', // Store的名称,用于调试 initialState, reducers: { // 4. 定义Reducer:一个纯函数,接收当前状态和Action,返回新状态 increment(state) { // 这里直接返回一个新的状态对象,遵循不可变数据原则 return { ...state, count: state.count + 1 }; }, decrement(state) { return { ...state, count: state.count - 1 }; }, add(state, payload: number) { // Reducer可以接收一个payload(负载) return { ...state, count: state.count + payload }; }, }, // effects 部分我们稍后添加 }); export default counterStore;

看,短短二十几行代码,我们就定义了一个完整的、类型安全的计数器状态管理单元。createStore函数返回的counterStore对象,就是我们之前说的“服务”。它现在拥有以下能力:

  • counterStore.getState(): 获取当前状态。
  • counterStore.dispatch(actionType, payload?): 派发一个Action来触发状态更新。例如counterStore.dispatch('increment')
  • counterStore.subscribe(listener): 订阅状态变化。

3.2 在React组件中使用:创建自定义Hook

为了在React中优雅地使用Biscuit Store,我们通常会创建一个自定义Hook。这是连接Biscuit和React组件的最佳实践。

// useCounter.ts import { useEffect, useState } from 'react'; import counterStore from './counterStore'; export function useCounter() { // 使用React的useState来同步Biscuit Store的状态 const [state, setState] = useState(counterStore.getState()); useEffect(() => { // 组件挂载时,订阅Store的状态变化 const unsubscribe = counterStore.subscribe((newState) => { setState(newState); }); // 组件卸载时,取消订阅 return unsubscribe; }, []); // 空依赖数组,确保只订阅一次 // 将dispatch方法和状态一起返回给组件使用 return { state, dispatch: counterStore.dispatch, }; }

现在,在React组件中使用就非常简单了:

// CounterComponent.tsx import React from 'react'; import { useCounter } from './useCounter'; export const CounterComponent: React.FC = () => { const { state, dispatch } = useCounter(); return ( <div> <h1>Count: {state.count}</h1> <button onClick={() => dispatch('increment')}>+</button> <button onClick={() => dispatch('decrement')}>-</button> <button onClick={() => dispatch('add', 5)}>Add 5</button> </div> ); };

整个流程清晰明了:组件通过Hook获取状态和dispatch方法,用户交互触发dispatch,Biscuit Store内的reducer计算新状态,然后通知所有订阅者(我们的Hook),Hook更新组件的本地state,触发重新渲染。

注意:上面的自定义Hook是一个基础实现。在实际项目中,为了优化性能(避免不必要的重渲染),你可能需要使用Biscuit提供的React绑定库(如果存在)或者使用更精细的订阅逻辑(例如只订阅状态的一部分)。社区也可能有相关的集成方案。

4. 进阶实战:处理异步操作与复杂状态

计数器只是一个玩具。让我们看一个更接近真实场景的例子:一个用户待办事项(Todo)列表应用,它需要从后端API获取数据、添加新事项、切换完成状态。

4.1 定义包含副作用的Todo Store

// todoStore.ts import { createStore } from 'biscuit-store'; // 1. 定义数据类型 interface TodoItem { id: string; text: string; completed: boolean; } interface TodoState { items: TodoItem[]; loading: boolean; error: string | null; } const initialState: TodoState = { items: [], loading: false, error: null, }; // 模拟一个简单的API客户端 const todoApi = { fetchTodos: (): Promise<TodoItem[]> => new Promise(resolve => setTimeout(() => resolve([{id: '1', text: 'Learn Biscuit', completed: false}]), 500)), addTodo: (text: string): Promise<TodoItem> => new Promise(resolve => setTimeout(() => resolve({id: Date.now().toString(), text, completed: false}), 300)), toggleTodo: (id: string): Promise<TodoItem> => new Promise(resolve => setTimeout(() => resolve({id, text: 'Toggled', completed: true}), 200)), }; const todoStore = createStore<TodoState>({ name: 'todos', initialState, reducers: { // 同步Reducer:开始加载 fetchStart(state) { return { ...state, loading: true, error: null }; }, // 同步Reducer:获取成功 fetchSuccess(state, payload: TodoItem[]) { return { ...state, loading: false, items: payload }; }, // 同步Reducer:操作失败 operationFailure(state, payload: string) { return { ...state, loading: false, error: payload }; }, // 同步Reducer:添加本地Todo(乐观更新) addTodoLocal(state, payload: TodoItem) { return { ...state, items: [...state.items, payload] }; }, // 同步Reducer:切换本地Todo状态 toggleTodoLocal(state, payload: string) { // payload是todo的id return { ...state, items: state.items.map(item => item.id === payload ? { ...item, completed: !item.completed } : item ), }; }, }, effects: { // 4. 定义Effect:获取待办事项列表 async fetchTodos(dispatch) { try { dispatch('fetchStart'); // 派发同步Action,更新loading状态 const todos = await todoApi.fetchTodos(); // 执行异步操作 dispatch('fetchSuccess', todos); // 成功,派发同步Action更新数据 } catch (err) { dispatch('operationFailure', 'Failed to fetch todos'); // 失败,更新错误状态 } }, // Effect:添加待办事项(带乐观更新) async addTodo(dispatch, payload: string) { // payload是新todo的文本 const optimisticTodo: TodoItem = { id: `temp_${Date.now()}`, text: payload, completed: false, }; // 1. 先乐观更新UI dispatch('addTodoLocal', optimisticTodo); try { // 2. 发起真实网络请求 const savedTodo = await todoApi.addTodo(payload); // 3. 请求成功,用服务器返回的数据替换掉临时的乐观更新数据 // 这里需要另一个Reducer来处理替换逻辑,为了简洁,我们假设addTodoLocal能处理,实际可能需要一个`replaceTodo`的reducer // 为了示例,我们简化处理:重新获取列表。实际项目应根据API设计调整。 dispatch('fetchTodos'); // 重新获取最新列表 } catch (err) { // 4. 请求失败,回滚乐观更新 dispatch('operationFailure', 'Failed to add todo'); // 需要额外的reducer来回滚,这里同样简化。实际项目中,回滚逻辑是必须的。 } }, // Effect:切换待办事项状态 async toggleTodo(dispatch, payload: string) { // payload是todo的id // 乐观更新 dispatch('toggleTodoLocal', payload); try { await todoApi.toggleTodo(payload); // 成功后可选择不做事,或重新获取确认状态 } catch (err) { dispatch('operationFailure', 'Failed to toggle todo'); // 同样,这里需要回滚乐观更新 } }, }, }); export default todoStore;

这个todoStore展示了Biscuit处理复杂异步流程的能力:

  1. 清晰的流程:每个异步操作(Effect)都遵循“开始 -> 执行 -> 成功/失败”的模式,并通过dispatch同步Action来驱动UI状态变化。
  2. 乐观更新:在addTodoEffect中,我们立即在本地添加一个临时事项,提升用户体验,然后在请求成功后用真实数据替换或失败后回滚。这体现了Biscuit将副作用与状态更新逻辑集中管理的好处。
  3. 错误处理:统一的错误状态管理和处理。

4.2 组合Store:构建模块化应用状态

一个真实应用不会只有一个Store。Biscuit鼓励你将应用状态按功能模块拆分成多个Store。例如,除了todoStore,你可能还有userStore(管理用户信息)、filterStore(管理列表过滤条件)。

这些Store之间如何通信?Biscuit不提倡Store之间直接相互调用或修改状态。更优雅的方式是通过“依赖”“消息传递”

  • 方式一:在Effect中调用其他Store的Effect(如果它们需要协作)。这要求Store实例在同一个上下文中可用。
  • 方式二:通过父级组件或一个根级的协调器来组织多个Store的交互。例如,在一个页面组件中,同时使用useTodouseUser两个Hook,当用户登录后,再触发获取该用户的待办事项。
  • 方式三:创建一个顶层的“应用Store”,将其他Store作为其状态的一部分或通过某种方式关联起来。这种方式更复杂,但适合全局状态紧密耦合的场景。

Biscuit本身的API很精简,它提供了构建模块(Store)的基础,如何架构这些模块之间的关系,给了开发者很大的灵活度。这也是其“可组合性”理念的体现——每个Store都是独立的乐高积木,你可以自由决定如何搭建你的城堡。

5. 测试策略:如何为Biscuit Store编写单元测试

可测试性是Biscuit设计的核心优势之一。因为Reducer是纯函数,Effect虽然包含副作用,但其输入输出和对外部世界的依赖(如API调用)是可以被模拟(Mock)的,所以测试变得非常直接。

5.1 测试Reducer

Reducer是纯函数,测试起来最简单。给定一个输入状态和一个Action,断言输出状态是否符合预期。

// todoStore.test.ts - 测试Reducer import todoStore from './todoStore'; describe('Todo Store Reducers', () => { const initialState = { items: [], loading: false, error: null }; test('fetchStart reducer should set loading to true and clear error', () => { const stateWithError = { ...initialState, error: 'Previous error' }; const newState = todoStore.reducers.fetchStart(stateWithError); expect(newState.loading).toBe(true); expect(newState.error).toBeNull(); // 确保是不可变更新 expect(newState).not.toBe(stateWithError); }); test('addTodoLocal reducer should add a new todo to items', () => { const newTodo = { id: '1', text: 'Test', completed: false }; const newState = todoStore.reducers.addTodoLocal(initialState, newTodo); expect(newState.items).toHaveLength(1); expect(newState.items[0]).toEqual(newTodo); }); });

5.2 测试Effect

测试Effect的关键是模拟(Mock)掉所有外部依赖(如API模块、dispatch函数),然后验证在特定输入下,是否按预期调用了这些依赖。

// todoStore.test.ts - 测试Effect import todoStore from './todoStore'; import { todoApi } from './todoStore'; // 需要将api导出以便Mock jest.mock('./todoStore'); // 使用Jest Mock整个模块,或者单独Mock `todoApi` describe('Todo Store Effects', () => { let mockDispatch: jest.Mock; beforeEach(() => { mockDispatch = jest.fn(); jest.clearAllMocks(); }); test('fetchTodos effect should dispatch start, success on API success', async () => { // 1. Mock API成功返回 const mockTodos = [{ id: '1', text: 'Mocked', completed: false }]; (todoApi.fetchTodos as jest.Mock).mockResolvedValue(mockTodos); // 2. 执行Effect await todoStore.effects.fetchTodos(mockDispatch); // 3. 验证dispatch被调用的顺序和参数 expect(mockDispatch).toHaveBeenCalledTimes(2); expect(mockDispatch).toHaveBeenNthCalledWith(1, 'fetchStart'); expect(mockDispatch).toHaveBeenNthCalledWith(2, 'fetchSuccess', mockTodos); }); test('fetchTodos effect should dispatch start, failure on API error', async () => { // 1. Mock API失败 (todoApi.fetchTodos as jest.Mock).mockRejectedValue(new Error('Network error')); // 2. 执行Effect await todoStore.effects.fetchTodos(mockDispatch); // 3. 验证 expect(mockDispatch).toHaveBeenCalledTimes(2); expect(mockDispatch).toHaveBeenNthCalledWith(1, 'fetchStart'); expect(mockDispatch).toHaveBeenNthCalledWith(2, 'operationFailure', 'Failed to fetch todos'); }); });

通过这样的测试,你可以确保业务逻辑的每个分支都按预期工作,并且在重构代码时充满信心。

6. 性能优化与生产环境实践

当应用规模增长时,状态管理的性能也需要被考虑。Biscuit本身非常轻量,性能开销极小,但结合React等视图库使用时,有一些最佳实践可以遵循。

6.1 选择性订阅与状态切片

在之前的useCounterHook示例中,我们订阅了整个Store的状态变化。如果Store状态很大,但组件只关心其中一小部分(比如todoStore中的loading状态),那么每次其他不相关的状态变化(如items更新)都会导致该组件重新渲染。

优化方案:创建选择器(Selector)和细粒度订阅。

Biscuit Store的subscribe方法可以监听特定的状态路径变化(如果Store支持),或者我们可以在自定义Hook中实现选择器逻辑。

// useTodoLoading.ts - 一个只订阅loading状态的Hook import { useEffect, useState } from 'react'; import todoStore from './todoStore'; export function useTodoLoading() { const [loading, setLoading] = useState(todoStore.getState().loading); useEffect(() => { // 监听整个状态,但只在loading字段变化时更新 const unsubscribe = todoStore.subscribe((newState) => { // 简单的相等性检查,避免不必要的setState if (newState.loading !== loading) { setLoading(newState.loading); } }); return unsubscribe; }, [loading]); // 将loading作为依赖,确保回调函数使用最新的值 return loading; }

对于更复杂的选择逻辑,可以考虑使用类似reselect的库来创建记忆化(memoized)选择器,避免在每次订阅回调中都进行昂贵的计算。

6.2 不可变数据与更新性能

Biscuit的Reducer要求返回新的状态对象。在更新深层嵌套的状态时,如果不注意,容易写出性能不佳的代码。

// 不佳:每次更新都创建全新的完整状态树 reducers: { updateDeepField(state, payload) { return { ...state, deep: { ...state.deep, nested: { ...state.deep.nested, field: payload, }, }, }; }, }

对于复杂状态,可以考虑使用像Immer这样的库。Biscuit可以与Immer完美结合。你可以在Reducer中直接“修改”一个草稿状态,Immer会负责生成不可变的更新。

import { produce } from 'immer'; const store = createStore({ // ... initialState, reducers: { updateDeepField: produce((draftState, payload) => { draftState.deep.nested.field = payload; // 直接修改,语法简洁 }), }, });

使用Immer后,代码可读性大幅提升,且性能通常比手写展开运算符更好,因为Immer会进行结构共享。

6.3 开发工具与调试

良好的开发体验离不开调试工具。虽然Biscuit可能没有像Redux DevTools那样庞大的生态系统,但你可以利用其简单的API自己实现日志记录。

// 创建一个带日志中间件的enhancer(增强器) const withLogger = (storeCreator) => (config) => { const store = storeCreator(config); const originalDispatch = store.dispatch; store.dispatch = (actionType, payload) => { console.group(`Action: ${actionType}`); console.log('Payload:', payload); console.log('State Before:', store.getState()); const result = originalDispatch(actionType, payload); console.log('State After:', store.getState()); console.groupEnd(); return result; }; return store; }; // 使用enhancer创建store const loggedStore = createStore(withLogger)({ name: 'loggedCounter', initialState: { count: 0 }, reducers: { /* ... */ }, });

在生产环境中,记得移除或禁用这类日志。

7. 与主流状态管理方案的对比与选型思考

在技术选型时,我们总免不了比较。Biscuit在状态管理生态中处于什么位置?让我们将其与几个主流方案进行简要对比。

特性BiscuitRedux (with Toolkit)ZustandValtio / MobX
核心理念状态即服务,可组合Store单一状态树,严格单向数据流极简,基于Hook的原子状态可变状态,响应式代理
学习曲线中等中等偏高(RTK简化后降低)低(但理念不同)
样板代码较少传统Redux多,RTK少极少极少
TypeScript支持优秀,类型推断强优秀(RTK)优秀优秀
异步处理内置Effects模型需搭配Thunk/Saga等中间件可在Store内直接写异步在Action中直接处理
模块化/组合,Store天然独立通过Slice组合,但仍在单一Store内可创建多个独立Store可创建多个Store
性能优化需手动优化订阅需搭配Selector(如Reselect)自动优化(基于Hook)细粒度响应,自动优化
适用场景中大型应用,强调模块化、类型安全、可测试性超大型应用,需要严格架构、强大中间件生态和时光旅行调试中小型应用,追求快速开发、简单直观希望用可变语法,或需要细粒度响应更新的应用

选型建议:

  • 选择Biscuit,如果你:正在构建一个中型到大型的TypeScript应用,非常看重代码的组织结构、模块化和可测试性,希望有一个清晰、不“魔改”的异步处理模型,并且愿意接受相对较新的社区生态。
  • 选择Redux Toolkit,如果你:项目非常庞大,需要最成熟、最稳定的解决方案,依赖其强大的中间件生态(如Redux Saga用于复杂异步流),或者必须使用Redux DevTools进行深度调试。
  • 选择Zustand,如果你:项目规模不大,或者你极度厌恶样板代码,想要一个上手即用、API极其简单、与React Hook结合天衣无缝的方案。
  • 选择Valtio/MobX,如果你:更习惯于可变(mutable)状态的操作方式,或者应用中有大量细粒度的、相互关联的状态更新,希望获得自动的、最优的渲染性能。

Biscuit的优势在于它在“结构清晰度”、“类型安全”、“可测试性”和“开发体验”之间找到了一个很好的平衡点。它不强求你遵循一个庞大的框架,而是提供了一套坚实、可组合的原语,让你能够构建出适合自己项目复杂度的架构。

8. 常见问题与避坑指南

在实际使用Biscuit的过程中,我总结了一些常见问题和注意事项,希望能帮你少走弯路。

8.1 Effect中的无限循环

这是一个新手容易踩的坑。在Effect中调用dispatch,如果这个Action又触发了一个Effect,而这个Effect又dispatch了第一个Action,就会形成无限循环。

// 错误示例:无限循环 effects: { async effectA(dispatch) { const data = await fetchSomething(); dispatch('actionB', data); // 触发effectB }, async effectB(dispatch, payload) { // 处理payload... dispatch('actionA'); // 又触发effectA,循环开始! }, }

解决方案:仔细规划Action和Effect的触发链。避免形成闭环。如果两个操作相互依赖,考虑将它们合并到一个Effect中,或者引入一个中间状态来打破循环。

8.2 状态更新未触发组件重渲染

如果你在React中使用自定义Hook订阅,但状态更新后组件没渲染,请检查:

  1. Hook的依赖数组useEffect的依赖数组是否包含了所有变化的值?上面的useTodoLoading例子中,我们将loading作为依赖,确保了回调函数能拿到最新的值进行比较。
  2. 状态相等性判断:在subscribe回调中,你是否做了深层比较?对于对象或数组,即使内容变了,如果你直接setState(newState),React可能因为浅比较认为状态没变而不更新。这时需要使用选择器或确保返回的是新对象。
  3. Store实例一致性:确保组件树中使用的都是同一个Store实例。如果每次渲染都创建新的Store实例,订阅自然会失效。

8.3 处理复杂的嵌套状态更新

当状态层级很深时,手写展开运算符(...)非常繁琐且易错。强烈推荐使用Immer,正如前面性能优化部分提到的。它能让Reducer的代码变得极其简洁和直观,几乎就像在直接修改状态一样,同时保证了不可变性。

8.4 如何共享Store实例

在大型应用中,你可能需要在多个模块中访问同一个Store。简单的做法是将其导出为一个单例(就像我们例子中的counterStore)。对于需要动态创建Store的场景(如每个页面实例需要一个独立的Store),可以考虑使用React Context来提供Store实例。

// StoreContext.tsx import React, { createContext, useContext } from 'react'; import counterStore from './counterStore'; const StoreContext = createContext(counterStore); export const StoreProvider: React.FC<{children: React.ReactNode}> = ({ children }) => { return ( <StoreContext.Provider value={counterStore}> {children} </StoreContext.Provider> ); }; // 自定义Hook,用于在组件中获取Store export function useStore() { const store = useContext(StoreContext); if (!store) { throw new Error('useStore must be used within a StoreProvider'); } return store; }

然后在应用根组件包裹StoreProvider,任何子组件都可以通过useStore()Hook来获取和操作Store。

8.5 服务端渲染(SSR)支持

Biscuit作为一个纯状态管理库,本身不直接处理SSR。在SSR场景(如Next.js)下,关键是要保证每个请求都有一个独立的Store实例,避免状态在用户间泄露。

基本思路是:在服务器端处理每个请求时,创建一个新的Store实例,并用初始数据(可能来自API)填充它。然后,将这个初始状态序列化,通过window.__INITIAL_STATE__等方式注入到HTML中。在客户端水合(hydrate)时,用这个初始状态来创建客户端的Store实例。

这个过程需要一些样板代码来协调,但原理是清晰的。社区未来可能会出现更集成的Biscuit SSR解决方案。

经过一段时间的实践,Biscuit给我的感觉更像是一个精心设计的工具,而不是一个沉重的框架。它用最小的概念(Store, Reducer, Effect)为你提供了构建健壮应用状态所需的一切,同时把架构设计的自由留给了你。它的类型安全特性在TypeScript项目中尤其令人愉悦,几乎消除了所有与状态相关的运行时类型错误。如果你正在为一个新项目选择状态管理方案,或者对现有项目的状态管理混乱感到头疼,不妨花一个下午试试这块名为Biscuit的“小饼干”,它可能会给你带来意想不到的清爽体验。

http://www.jsqmd.com/news/761465/

相关文章:

  • 别再只懂 -x preset 了!Minimap2 实战:手把手教你调参搞定 PacBio HiFi 数据比对
  • 避开Web端协议坑:手把手教你用海康设备网络SDK搞定语音对讲(附Windows/Linux双环境配置)
  • Visual Studio 2022里遇到C6262警告别慌,手把手教你三种方法把大数组从栈搬到堆上
  • Dify缓存雪崩/穿透/击穿终极防御体系(2026新版TTL+布隆+本地多级缓存三重熔断)
  • 避坑指南:用Docker和源码两种方式搞定MMDetection3D环境(附CUDA、PyTorch版本匹配清单)
  • 思源宋体:开源中文字体的全栈应用实战
  • 别再为UniApp H5跨域发愁了!manifest.json和vue.config.js两种代理配置保姆级对比
  • Arm Neoverse N1 PMU架构与性能监控实践
  • 人形机器人自适应全身操作框架:强化学习与多模态感知融合
  • FastAPI 查询参数
  • 除了中科大和阿里云,Kali换源还有哪些冷门但好用的选择?实测对比
  • 手把手教你用MSP430单片机驱动DS18B20:从Proteus仿真到LCD1602显示的保姆级教程
  • 别光会跑压测!JMeter线程组参数(线程数、Ramp-Up)到底怎么设才合理?
  • RISC-V向量扩展V1.0 Spec精读:vtype、vlenb这些CSR寄存器到底怎么用?
  • Vivado里找不到ISE的IP怎么办?用源码重建AXI Slave Burst等老IP的实战记录
  • PHP 8.9垃圾回收机制重大升级:3个被官方文档隐藏的refcount优化技巧,99%开发者尚未启用
  • CVAT团队标注实战:如何用Task和Jobs功能搞定多人协同与质量管理
  • 手把手教你用FPGA驱动SHT30/SHT35温湿度传感器(附Verilog代码)
  • GD32外部中断EXTI保姆级教程:从GPIO映射到中断服务函数,手把手搞定按键计数
  • ROS2 Humble开发避坑:从Node到Component的迁移指南(含跨平台编译visibility_control.h详解)
  • 从ARM转战RISC-V踩坑记:CH32V307中断只进一次?一个关键字搞定
  • 别再死记硬背了!用Python代码实现NFA转DFA,理解编译原理核心算法
  • Claude Code 如何通过 Taotoken 配置 API 密钥与聚合端点实现快速接入
  • 多模态视频超分辨率技术:原理、应用与优化
  • MoeCTF 2025 Writeup
  • 别再手动改yaml了!Dify 2026审计配置自动化脚本开源实测:3分钟生成符合等保三级要求的全链路配置包
  • 2026海水淡化不锈钢厂家地址:S31254材质保真、S31254焊管、S31254现货供应、S31254管材选择指南 - 优质品牌商家
  • 告别毕业论文焦虑:用百考通AI一站式搞定本科论文终稿
  • VLA-4D框架:让机器人理解复杂指令的4D视觉语言动作模型
  • Docker Compose 与 Kubernetes 在小型项目部署中的选型对比