谈谈复杂应用的状态管理(上):为什么是 Zustand

发表于 1年以前  | 总阅读数:826 次

作为一名主业做设计,业余搞前端的小菜鸡,到 2020 年底为止都是用云谦大佬的 dva 一把梭。当时整体的使用体验还是挺好的,对于我这样的前端菜鸡上手门槛低,而且学一次哪都可用,当时从来没愁过状态管理。

至今 Kitchen3 里仍然躺着用 dva 做状态管理的功能模块,写于 2020 年

直到 hooks 横空出世, TypeScript 逐步流行。一方面,从 react hooks 出来以后,大量的文章开始鼓吹「你不需要 Redux」、「useState + Context」完全可用、「next-unstated」YYDS 等等。另一方面,由于 Dva 不再维护,其在 ts 下的都没有任何提示的问题也逐步暴露。

在尝试一些小项目中使用 hooks 后感觉还行之后,作为小萌新的我也全面转向了 hooks 的怀抱。中间其实一直没怎么遇到问题,因为大部分前端应用的复杂度也就那样,hooks 问题不大。然后呢?然后从去年开始就在复杂应用里踩坑了。

复杂应用的状态管理天坑

ProEditor 是内部组件库 TechUI Studio 的编辑器组件。

业务组件 ProEditor 就是一个很典型的例子。由于 ProEditor 是个编辑器,对用户来说编辑体验非常重要,是一个重交互操作的应用,这就会牵扯到大量的状态管理需求。

先简单来列下 ProEditor 的状态管理需求有哪些:

❶ Editor 容器状态管理与组件(Table)状态管理拆分,但可联动消费;

容器状态负责了一些偏全局配置的状态维护,比如画布、代码页的切换,是否激活画布交互等等,而组件的状态则是保存了组件本身的所有配置和状态。

这么做的好处在于不同组件可能会有不同的状态,而 Editor 的容器状态可以复用,比如做 ProForm 的时候,Editor 的容器仍然可以是同一个,组件状态只需额外实现 ProForm 的 Store 即可。

从上图可以看到,Table 的状态就是 Editor 的 config 字段,当 Table 改时,会触发 Editor 的 config 字段同步更新。当 Editor 更新时,也会触发该数据更新。

最初的版本,我使用了 Provider + Context 的方式来做全局状态管理。大概的写法是这样的:

// 定义
export const useStudioStore = (props?: ProEditorProps) => {
  // ...
  const tableStore = useTableStore(props?.value);
  const [tabKey, switchTab] = useState(TabKey.canvas);
  const [activeConfigTab, switchConfigTab] = useState<TableConfigGroup>(TableConfigGroup.Table);
  // ...
}

export const StudioStore = createContextStore(useStudioStore, {});

// 消费 
const NavBar: FC<NavBarProps> = ({ logo }) => {
  const { tabKey } = useContext(StudioStore);
  return ...
}

由于这一版是 Context 一杆推到底,这造成了一些很离谱的交互反馈,就是每一次点击其他任何地方(例如画布代码、组件的配置项),都会造成面板的 Tabs 重新渲染(左下图)。右下图是相应的重渲染分析图,可以看到任何动作都造成了重新所有页面元素的重渲染。而这还是最早期的 demo 版本,功能和数据量的才实现到 20% 左右。所以可以预见到如果不做任何优化,使用体验会差到什么程度。

❷ 需要进行复杂的数据处理

ProEditor 针对表格编辑,做了大量的数据变换操作。比如 ProTable 中针对 columns 这个字段的更新就有 14 种操作。比如其中一个比较容易被感知的updateColumnByOneAPI 就是基于 oneAPI 的字段信息更新,细颗粒度地调整 columns 里的字段信息。而这样的字段修改类型的 store,在 ProEditor 中除了 columns 还有一个 data

当时,为了保证数据变更方法的可维护性与 action 的不变性,我采用了 userReducer 做变更方法的管理。

因为一旦采用自定义 hooks ,就得写成下面这样才能保证不会重复渲染,会造成极大的心智负担,一旦出现数据不对的情况,很难排查到底是哪个方法或者依赖有问题。

// 自定 hook 的写法
const useDataColumns = () => {
  const createOrUpdateColumnsByMockData = useCallback(()=>{
    // ...
  },[a,b]);
  const createColumnsByOneAPI = useCallback(()=>{
    // ...
  },[c,d]);
  const updateColumnsByOneAPI = useCallback(()=>{
    // ...
  },[a,b,c,d]);
  // ...
}

但 useReducer 也有很大的局限性,例如不支持异步函数、不支持内部的 reducer 互相调用,不支持和其他 state 联动(比如要当参数穿进去才可用),所以也不是最优解。

❸ 是个可被外部消费的组件

一旦提到组件,势必要提非受控模式和受控模式。为了支持好我们自己的场景,且希望把 ProEditor 变成一个好用的业务组件,所以我们做了受控模式,毕竟一个好用的组件一定是要能同时支持好这两种模式的。

在实际场景下,我们既需要配置项(config)受控,同时也需要画布交互状态(interaction)受控,例如下面的场景:在激活某个单元格状态时点击生成,我们需要将这个选中状态进行重置,才能生成符合预期的设计稿。

所以为了支持细颗粒度的受控能力,我们提供了多个受控值,供外部受控模式。

// 自定 hook 的写法
const useDataColumns = () => {
  const createOrUpdateColumnsByMockData = useCallback(()=>{
    // ...
  },[a,b]);
  const createColumnsByOneAPI = useCallback(()=>{
    // ...
  },[c,d]);
  const updateColumnsByOneAPI = useCallback(()=>{
    // ...
  },[a,b,c,d]);
  // ...
}

但当我们一开始写好这个受控 api,得到结果是这样的:

对,你没看错,死循环了。遇到这个问题时让人头极度秃,因为原本以为是个很简单的功能,但是在 React 生命周期里的表现让人费解,尤其是使用 useEffect 做状态管理的时候。

// 导致死循环的写法
const useTableStore = (state: Partial<Omit<ProTableConfigStore, 'columns' | 'data'>>) => {
  const { defaultConfig, config: outsourceValue, mode } = props;
  const { columns, isEmptyColumns, dispatchColumns } = useColumnStore(defaultConfig?.columns, mode);
  // 受控模式 内部值与外部双向通信
  useEffect(() => {
    // 没有外部值和变更时不更改
    if (!outsourceValue) return;
    // 相等值的时候不做更新
    if (isEqual(dataStore, outsourceValue)) return;
    if (outsourceValue.columns) {
      dispatchColumns({ type: 'setAll', columns: outsourceValue.columns });
    }
  }, [dataStore, outsourceValue]);

  const dataStore = useMemo(() => {
    const v = { ...store, data, columns } as ProTableConfigStore;
    // dataStore 变更时需要对外变更一次
    if (props.onChange && !isEqual(v, outsourceValue)) {
      props.onChange?.({
        config: v,
        props: tableAsset.generateProps(v),
        isEmptyColumns,
      });
    }
    return v;
  }, [data, store, columns, outsourceValue]);

  // ...
}

造成上述问题的原因大部分都是因为组件内 onChange 的时机设置。一旦代码里用 useEffect 的方式去监听变更触发 onChange,有很大的概率会造成死循环。

❹ 未来还希望能支持撤销重做、快捷键等能力

毕竟,现代的编辑器都是支持快捷键、历史记录、多人协同等增强型的功能的。这些能力怎么在编辑器的状态管理中以低成本、易维护的方式进行实施,也非常重要。

总之,开发 ProEditor 的经历,一句话的血泪教训就是:

复杂应用的状态管理真的不能裸写 hooks!

复杂应用的状态管理真的不能裸写 hooks!

复杂应用的状态管理真的不能裸写 hooks!

那些鼓吹裸写 hooks 的人大概率是没遇到过复杂 case,性能优化、受控、action 互调、数据切片、状态调试等坑,每一项都不是好惹的主,够人喝上一壶。

为什么是 Zustand ?

其实,复杂应用只是开发者状态管理需求的集中体现。如果我们把状态管理当成一款产品来设计,我们不妨看看开发者在状态管理下的核心需求是什么。

我相信通过以下这一串分析,你会发现 zustand 是真真正正满足「几乎所有」状态管理需求的工具,并且在很多细节上做到了体验更优。

官网:https://zustand-demo.pmnd.rs/

❶ 状态共享

状态管理最必要的一点就是状态共享。这也是 context 出来以后,大部分文章说不需要 redux 的根本原因。因为context 可以实现最最基础的状态共享。但这种方法(包括 redux 在内),都需要在最外层包一个 Provider。Context 中的值都在 Provider 的作用域下有效。

// 导致死循环的写法
const useTableStore = (state: Partial<Omit<ProTableConfigStore, 'columns' | 'data'>>) => {
  const { defaultConfig, config: outsourceValue, mode } = props;
  const { columns, isEmptyColumns, dispatchColumns } = useColumnStore(defaultConfig?.columns, mode);
  // 受控模式 内部值与外部双向通信
  useEffect(() => {
    // 没有外部值和变更时不更改
    if (!outsourceValue) return;
    // 相等值的时候不做更新
    if (isEqual(dataStore, outsourceValue)) return;
    if (outsourceValue.columns) {
      dispatchColumns({ type: 'setAll', columns: outsourceValue.columns });
    }
  }, [dataStore, outsourceValue]);

  const dataStore = useMemo(() => {
    const v = { ...store, data, columns } as ProTableConfigStore;
    // dataStore 变更时需要对外变更一次
    if (props.onChange && !isEqual(v, outsourceValue)) {
      props.onChange?.({
        config: v,
        props: tableAsset.generateProps(v),
        isEmptyColumns,
      });
    }
    return v;
  }, [data, store, columns, outsourceValue]);

  // ...
}

而 zustand 做到的第一点创新就是:默认不需要 Provider。直接声明一个 hooks 式的 useStore 后就可以在不同组件中进行调用。它们的状态会直接共享,简单而美好。

// Zustand 状态共享

// store.ts
import create from 'zustand'

export const useStore = create(set => ({
  count: 1,
  inc: () => set(state => ({ count: state.count + 1 })),
}))

// Control.tsx
import { useStore } from './store';

function Control() {
  return <button onClick={()=>{
    useStore.setState((s)=>({...s,count: s.count - 5 }))
    }}>-5</button>
}

// AnotherControl.tsx
import { useStore } from './store';

function AnotherControl() {
  const inc = useStore(state => state.inc)
  return <button onClick={inc}> +1 </button>
}

// Counter.tsx
import { useStore } from './store';

function Counter() {
  const { count } = useStore()
  return <h1>{count}</h1>  
}

由于没有 Provider 的存在,所以声明的 useStore 默认都是单实例,如果需要多实例的话,zustand 也提供了对应的 Provider 的书写方式,这种方式在组件库中比较常用。ProEditor 也是用的这种方式做到了多实例。

此外,zustand 的 store 状态既可以在 react 世界中消费,也可以在 react 世界外消费。

❷ 状态变更

状态管理除了状态共享外,另外第二个极其必要的能力就是状态变更。在复杂的场景下,我们往往需要自行组织相应的状态变更方法,不然不好维护。这也是考验一个状态管理库好不好用的一个必要指标。

hooks 的 setState 是原子级的变更状态,hold 不住复杂逻辑;而 useReducer 的 hooks 借鉴了 redux 的思想,提供了 dispatch 变更的方式,但和 redux 的 reducer 一样,这种方式没法处理异步,且没法互相调用,一旦遇上就容易捉襟见肘。

至于 redux ,哪怕是最新的 redux-toolkit 中优化大量 redux 的模板代码,针对同步异步方法的书写仍然让人心生畏惧。

// redux-toolkit 的用法

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// 1. 创建异步函数
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], loading: 'idle' },
  // 同步的 reducer 方法
  reducers: {

  },
  // 异步的 AsyncThunk 方法
  extraReducers: (builder) => {
    // 2. 将异步函数添加到 Slice 中
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      state.entities.push(action.payload)
    })
  },
})

// 3. 调用异步方法
dispatch(fetchUserById(123))

而在 zustand 中,函数可以直接写,完全不用区分同步或者异步,一下子把区分同步异步的心智负担降到了 0。

// zustand store 写法

// store.ts
import create from 'zustand';

const initialState = {
 // ...
};

export const useStore = create((set, get) => ({
  ...initialState,
  createNewDesignSystem: async () => {
    const { params, toggleLoading } = get();

    toggleLoading();
    const res = await dispatch('/hitu/remote/create-new-ds', params);
    toggleLoading();

    if (!res) return;

    set({ created: true, designId: res.id });
  },
  toggleLoading: () => {
    set({ loading: !get().loading });
  }
}));

// CreateForm.tsx
import { useStore } from './store';

const CreateForm: FC = () => {
  const { createNewDesignSystem } = useStore();

  // ...
}

另外一个让人非常舒心的点在于,zustand 会默认将所有的函数保持同一引用。所以用 zustand 写的方法,默认都不会造成额外的重复渲染。(PS:这里再顺带吹一下 WebStorm 对于函数和变量的识别能力,非常好用)

在下图可以看到,所有 zustand 的 useStore 出来的值或者方法,都是橙色的变量,具有稳定引用,不会造成不必要的重复渲染。

而状态变更函数的最后一个很重要,但往往又会被忽略的一点,就是方法需要调用当前快照下的值或方法

在常规的开发心智中,我们往往会在异步方法中直接调用当前快照的值来发起请求,或使用同步方法进行状态变更,这会有极好的状态内聚性。

比如说,我们有一个方法叫「废弃草稿」,需要获取当前的一个 id ,向服务器发起请求做数据变更,同时为了保证当前界面的数据显示有效性,变更完毕后,我们需要重新获取数据。

我们来看看 hooks 版本和 zustand 的写法对比,如下所示:

// zustand store 写法

// store.ts
import create from 'zustand';

const initialState = {
 // ...
};

export const useStore = create((set, get) => ({
  ...initialState,
  createNewDesignSystem: async () => {
    const { params, toggleLoading } = get();

    toggleLoading();
    const res = await dispatch('/hitu/remote/create-new-ds', params);
    toggleLoading();

    if (!res) return;

    set({ created: true, designId: res.id });
  },
  toggleLoading: () => {
    set({ loading: !get().loading });
  }
}));

// CreateForm.tsx
import { useStore } from './store';

const CreateForm: FC = () => {
  const { createNewDesignSystem } = useStore();

  // ...
}
// zustand 写法

const initialState = { designId: undefined, loading: false };

export const useStore = create((set, get) => ({
  ...initialState,
  deprecateDraft: async () => {
    set({ loading: true });
    const res = await dispatch('/hitu/remote/ds/deprecate-draft', get().designId);
    set({ loading: false });

    if (res) {
      message.success('草稿删除成功');
    }

    // 重新获取一遍数据
    get().refetch();
  },
  refetch: () => {
    if (get().designId) {
      mutateKitchenSWR('/hitu/remote/ds/versions', get().designId);
    }
  },
})

可以明显看到,光是从代码量上 zustand 的 store 比 hooks 减少了 30% 。不过另外容易被大家忽略,但其实更重要的是, hooks 版本中互调带来了引用变更的问题

由于 deprecateDraftrefetch 都调用了 designId,这就会使得当 designId 发生变更时,deprecateDraftrefetch 的引用会发生变更,致使 react 触发刷新。而这在有性能优化需求的场景下非常阴间,会让不该渲染的组件重新渲染。那这也是为什么react 要搞一个 useEvent 的原因(RFC)。

而 zustand 则把这个问题解掉了。由于 zustand 在 create 方法中提供了 get 对象,使得我们可以用 get 方法直接拿到当前 store 中最新的 state 快照。这样一来,变更函数的引用始终不变,而函数本身却一直可以拿到最新的值。

在这一趴,最后一点要夸 zustand 的是,它可以直接集成 useReducer 的模式,而且直接在官网提供了示例。这样就意味着之前在 ProEditor 中的那么多 action 可以极低成本完成迁移。

// columns 的 reducer 迁移

import { columnsConfigReducer } from './columns';

const createStore = create((set,get)=>({
  /**
   * 控制 Columns 的复杂数据变更方法
   */
  dispatchColumns: (payload) => {
    const { columns, internalUpdateTableConfig, updateDataByColumns } = get();
    // 旧的 useReducer 直接复用过来
    const nextColumns = columnsConfigReducer(columns, payload);

    internalUpdateTableConfig({ columns: nextColumns }, 'Columns 配置');

    updateDataByColumns(nextColumns);
  },
})

❸ 状态派生

状态派生是状态管理中一个不被那么多人提起,但是在实际场景中被大量使用的东西,只是大家没有意识到,这理应也是状态管理的一环。

状态派生可以很简单,也可以非常复杂。简单的例子,比如基于一个name 字段,拼接出对应的 url 。

复杂的例子,比如基于 rgb 、hsl 值和色彩模式,得到一个包含色彩空间的对象。

如果不考虑优化,其实都可以写一个中间的函数作为派生方法,但作为状态管理的一环,我们必须要考虑相应的优化。

在 hooks 场景下,状态派生的方法可以使用 useMemo,例如:

// hooks 写法

const App = () => {
  const [name,setName]=useState('')
  const url = useMemo(() => URL_HITU_DS_BASE(name || ''),[name])
  // ...
}

而 zustand 用了类似 redux selector 的方法,实现相应的状态派生,这个方式使得 useStore 的用法变得极其灵活和实用。而这种 selector 的方式使得 zustand 下细颗粒度的性能优化变为可能,且优化成本很低。

// zustand 的 selector 用法

// 写法1
const App = () => {
  const url = useStore( s => URL_HITU_DS_BASE(s.name || ''));
  // ...
}

// 写法2 将 selector 单独抽为函数
export const dsUrlSelector = (s) => URL_HITU_DS_BASE(s.name || '');
const App = () => {
  const url = useStore(dsUrlSelector);
  // ...
}

由于写法 2 可以将 selector 抽为独立函数,那么我们就可以将其拆分到独立文件来管理派生状态。由于这些selector 都是纯函数,所以能轻松实现测试覆盖。

❹ 性能优化

讲完状态派生后把 zustand 的 selector 能力后,直接很顺地就能来讲讲 zustand 的性能优化了。

在裸 hooks 的状态管理下,要做性能优化得专门起一个专项来分析与实施。但基于 zustand 的 useStore 和 selector 用法,我们可以实现低成本、渐进式的性能优化。

比如 ProEditor 中一个叫 TableConfig 的面板组件,对应的左下图中圈起来的部分。而右下图则是相应的代码,可以看到这个组件从 useStore 中 解构了 tabKeyinternalSetState 的方法。

然后我们用 useWhyDidYouUpdate 来检查下,如果直接用解构引入,会造成什么样的情况:

在上图中可以看到,虽然 tabsinternalSetState 没有变化,但是其中的 config 数据项(data、columns 等)发生了变化,进而使得 TableConfig 组件触发重渲染。

而我们的性能优化方法也很简单,只要利用 zustand 的 selector,将得到的对象聚焦到我们需要的对象,只监听这几个对象的变化即可。

// 性能优化方法

import shallow from 'zustand/shallow'; // zustand 提供的内置浅比较方法
import { useStore, ProTableStore } from './store'

const selector = (s: ProTableStore) => ({
  tabKey: s.tabKey,
  internalSetState: s.internalSetState,
});

const TableConfig: FC = () => {
  const { tabKey, internalSetState } = useStore(selector, shallow);
}

这样一来,TableConfig 的性能优化就做好了~

基于这种模式,性能优化就会变成极其简单无脑的操作,而且对于前期的功能实现的侵入性极小,代码的后续可维护性极高。

剩下的时间就可以和小伙伴去吹咱优雅的性能优化技巧了~( ̄︶ ̄)↗

就我个人的感受上, zustand 使用 selector 来作为性能优化的思路真的很精巧,就像是给函数式的数据流加上了一点点主观意愿上的响应式能力,堪称优雅。

❺ 数据分形与状态组合

如果子组件能够以同样的结构,作为一个应用使用,这样的结构就是分形架构。

数据分形在状态管理里我觉得是个比较高级的概念。但从应用上来说很简单,就是更容易拆分并组织代码,而且具有更加灵活的使用方式,如下所示是拆分代码的方式。但这种方式其实我还没大使用,所以不多展开了。

// 来自官方文档的示例

// https://github.com/pmndrs/zustand/blob/main/docs/typescript.md#slices-pattern

import create, { StateCreator } from 'zustand'

interface BearSlice {
  bears: number
  addBear: () => void
  eatFish: () => void
}
const createBearSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  BearSlice
> = (set) => ({
  bears: 0,
  addBear: () => set((state) => ({ bears: state.bears + 1 })),
  eatFish: () => set((state) => ({ fishes: state.fishes - 1 })),
})

interface FishSlice {
  fishes: number
  addFish: () => void
}
const createFishSlice: StateCreator<
  BearSlice & FishSlice,
  [],
  [],
  FishSlice
> = (set) => ({
  fishes: 0,
  addFish: () => set((state) => ({ fishes: state.fishes + 1 })),
})

const useBoundStore = create<BearSlice & FishSlice>()((...a) => ({
  ...createBearSlice(...a),
  ...createFishSlice(...a),
}))

我用的更多的是基于这种分形架构下的各种中间件。由于这种分形架构,状态就具有了很灵活的组合性,例如将当前状态直接缓存到 localStorage。在 zustand 的架构下, 不用额外改造,直接加个 persist 中间件就好。

// 使用自带的 Persist Middleware

import create from 'zustand'
import {  persist } from 'zustand/middleware'

interface BearState {
  bears: number
  increase: (by: number) => void
}

const useBearStore = create<BearState>(
  persist((set) => ({
    bears: 0,
    increase: (by) => set((state) => ({ bears: state.bears + by })),
  }))
)

在 ProEditor 中,我使用最多的就是 devtools 这个中间件。这个中间件具有的功能就是:将这个 Store 和Redux Devtools 绑定。

// devtools 中间件

// store 逻辑
const vanillaStore = (set,get)=> ({ 
  syncOutSource: (nextState) => {
    set({ ...get(), ...nextState }, false, `受控更新:${Object.keys(nextState).join(' ')}`);
  },
  syncOutSourceConfig: ({ config }) => {
    // ...
    set({ ...get(), ...config }, false, `受控更新: 组件配置`);
    // ...
  },
}); 

const createStore = create(
  devtools(vanillaStore, { name: 'ProTableStore' })
);

然后我们就可以在 redux-devtools 中愉快地查看数据变更了:

可能有小伙伴会注意到,为什么我这边的状态变更还有中文名,那是因为 devtools 中间件为 zustand 的 set 方法,提供了一个额外参数。只要设置好相应的 set 值的最后一个变量,就可以直接在 devtools 中看到相应的变更事件名称。

正是这样强大的分形能力,我们基于社区里做的一个 zundo 中间件,在 ProEditor 中提供了一个简易的撤销重做 的 Demo示例。

而实现核心功能的代码就只有一行~

PS:至于一开始提到的协同能力,我在社区中也有发现中间件 zustand-middleware-yjs (不过还没尝试)。

❻ 多环境集成( react 内外环境联动 )

实际的复杂应用中,一定会存在某些不在 react 环境内的状态数据,以图表、画布、3D 场景最多。一旦要涉及到多环境下的状态管理,可以让人掉无数头发。

而 zustand 说了,不慌,我已经考虑到了,useStore 上直接可以拿值,是不是很贴心~

// 官方示例

// 1. 创建Store
const useDogStore = create(() => ({ paw: true, snout: true, fur: true }))

// 2. react 环境外直接拿值
const paw = useDogStore.getState().paw

// 3. 提供外部事件订阅
const unsub1 = useDogStore.subscribe(console.log)

// 4. react 世界外更新值
useDogStore.setState({ paw: false })

const Component = () => {
  // 5. 在 react 环境内使用
  const paw = useDogStore((state) => state.paw)
  ...

虽然这个场景我还没遇到,但是一想到 zustand 在这种场景下也能支持,真的是让人十分心安。

其实还有其他不太值得单独提的点,比如 zustand 在测试上也相对比较容易做,直接用 test-library/react-hooks 即可。类型定义方面做的非常齐全……但到现在洋洋洒洒已经写了 6k 多字了,就不再展开了。

总结:zustand 是当下复杂状态管理的最佳选择

大概从去年12月份开始,我就一直在提炼符合我理想的状态管理库的需求,到看到 zustand 让我眼前一亮。而通过在 pro-editor 中大半年的实践验证,我很笃定地认为,zustand 就是我当下状态管理的最佳选择,甚至是大部分复杂应用的状态管理的最佳选择

本来最后还想讲讲,我是怎么样基于 Zustand 来做渐进式的状态管理的(从小应用到复杂应用的渐进式生长方案)。然后还想拿 ProEditor 为例讲讲 ProEditor 具体的状态管理是如何逐步生长的,包括如何组织的受控模式、如何集成 RxJS 处理复杂交互等等,算是几个比较有意思的点。不过限于篇幅原因,这些内容估计就得留到下次了。

本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/w0kpCe3GIn_EQ9DQCIkntw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236876次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  6889次阅读
 目录