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

React大型项目状态管理库如何选型? #15

Open
dravenww opened this issue Jul 22, 2021 · 0 comments
Open

React大型项目状态管理库如何选型? #15

dravenww opened this issue Jul 22, 2021 · 0 comments

Comments

@dravenww
Copy link
Owner

背景

由于要做一个使用起来比较舒服的轮子,最近研究了下React的状态管理库,当然,仅限在使用层面,也就是用着舒服的角度来选择到底使用哪个状态管理库。本着在Github上面看看React社区内状态管理库的流行程度和使用程度的层面,来进行选型,然后就有了这篇文章,关于我们最后选择了哪个,文章末尾告知。

选择库的原则如下:

  • 全面拥抱typescript,so选择的库需要对typescript支持友好
  • react自从16.8引入hooks,接下来就开始了函数式编程的时代,所以不要有class这玩意
  • 一定要使用简单,不要太复杂,使用起来很轻量,重点是舒服
  • 支持模块化,可以集中管理,原子性低
  • 支持esmodule,因为后面会考虑迁移到vite,虽然现在还是webpack

截止目前为止,在Github上面看了一下当前比较流行的几个状态管理库的star数和used by的数量,以及npm上面的周下载量(weekly downloads),这可以从某些方面说明明该框架的受欢迎程度,也有很小的可能性不准确,不过很大程度上,对框架选型是有所帮助的。

库名 github star github used npm 周下载量
mobx 23.9k 83.9k 671,787
redux-toolkit 5.9k 83.2k 755,564
recoil 13.5k 83.9k 95,245
zustand 9.4k 7.2k 104,682
rematch 7.3k 2.5k 33,810
concent 950 65 1,263

上面表格中,就是我们接下来要进行挑选的对象,到底中意哪个,还得看看使用起来的时候的姿势,哪个更加舒服。

mobx

mobx是一个非常优秀的react状态管理库,这毋容置疑,而且在Github上面,它的使用量也是做到了第一,官方文档地址zh.mobx.js.org。官网上面给的例子是非常简单的,大多数官网也都如此,可是我不需要简单的例子,我需要一个完整的项目的例子,于是参考了github上面的一个项目antd-pro-mobx。mobx需要搭配mobx-react的连接库一起来使用。

按照npm上面的推荐是要使用class + observe函数包裹的方式,最新版本v6:

import React from "react"
import ReactDOM from "react-dom"
import { makeAutoObservable } from "mobx"
import { observer } from "mobx-react"

// Model the application state.
class Timer {
    secondsPassed = 0

    constructor() {
        makeAutoObservable(this)
    }

    increase() {
        this.secondsPassed += 1
    }

    reset() {
        this.secondsPassed = 0
    }
}

const myTimer = new Timer()

// Build a "user interface" that uses the observable state.
const TimerView = observer(({ timer }) => (
    <button onClick={() => timer.reset()}>Seconds passed: {timer.secondsPassed}</button>
))

ReactDOM.render(<TimerView timer={myTimer} />, document.body)

// Update the 'Seconds passed: X' text every second.
setInterval(() => {
    myTimer.increase()
}, 1000)

新项目的从头开始,应该不会选择老版本的库区使用,一般会选择稳定的新版本的进行使用,关于typescript方面,看源码是已经在使用typescript来编写了,不过在官网和npm上面并没有看到typescript的蛛丝马迹,可能是还没发版吧。

我们对比我们的原则看下关于mobx:

  • 支持typescript -- NO
  • 使用函数式编程 -- NO
  • 使用舒服 -- OK
  • 原子性低问题 -- OK
  • 支持esmodule -- OK

关于mobx部分就暂且到这,说的不对的地方欢迎告知,确实是才疏学浅,没怎么用过这么流行的状态管理库。

reduxjs/toolkit

toolkit,暂且这么叫吧,redux官方状态管理库,cra模板redux(npx create-react-app --template redux)自带状态管理库,cra模板redux-ts(npx create-react-app --template redux-typescript)自带状态管理库.可能这两个模板也导致了toolkit的下载量和使用量非常大。也由于是redux官方库的原因,需要搭配react-redux来搭配使用。这些我们暂且不管,我们看下如何使用,亲测哈,如有使用不当,可以指出。

// index.ts 主入口文件
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootStore } from 'modules/store';
import { fetchInfo } from 'modules/counter';

function App(props: any) {
  const count = useSelector((state: RootStore) =>  state.counter.value);
  const dispatch = useDispatch();
  return (
    <div>
      hello home
      <hr/>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(fetchInfo(2234))}
        >
          fetchInfo
        </button>
        <div>
          <span>{count}</span>
        </div>
    </div>
  );
};

ReactDOM.render(
  <App/>,
  document.getElementById('root'),
);

上面是主入口文件的代码,可以看到这个使用方式还算是比较普遍,符合redux的使用方式。

// modules/store.ts
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import counter from './counter';
import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux';

const reducer = combineReducers({
  counter,
});

const store = configureStore({
  reducer,
});

export type RootStore = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootStore> = useSelector;
export default store;

上面是store主文件的代码,这其实也是官方给出的合理使用方式。

// modules/counter.ts
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

const namespace: string = 'counter';

export interface ICounter {
  value: number;
}

const initialState: ICounter = {
  value: 1,
};

// async 异步函数定义
export const fetchInfo = createAsyncThunk(`${namespace}/fetchInfo`, async (value: number) => {
  await sleep(1000);
  return {
    value: 9000 + value,
  };
});

// 创建带有命名空间的reducer
const counterSlice = createSlice({
  name: namespace,
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchInfo.pending, (state, action) => {
        state.value = 1000;
      })
      .addCase(fetchInfo.fulfilled, (state, {payload}) => {
        state.value = payload.value;
      })
      .addCase(fetchInfo.rejected, (state, {payload}) => {
        state.value = 500;
      });
  },
});

const { reducer } = counterSlice;
export default reducer;

上面是在实际module的使用,会产出一个reducer,唯一不优雅的地方在于extraReducers的调用方式采用了串联的方式,不过还可以通过对象的形式进行传递,不过在ts中支持不够友好,如下:

const counterSlice = createSlice({
  name: namespace,
  initialState,
  reducers: {},
  extraReducers: {
    [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => {
      state.value = 1000;
    },
    [fetchInfo.pending.type]: (state: Draft<ICounter>, { payload }: PayloadAction<ICounter>) => {
      state.value = payload.value;
    },
    [fetchInfo.pending.type]: (state: Draft<ICounter>, action: PayloadAction<ICounter>) => {
      state.value = 500;
    },
  },
});

可以看到上面换成了对象的方式,不过在函数里面需要自己去写好类型声明;而串行的方式,typescript已经自动推导出了函数所对应的参数类型。

我们对比我们的原则看下关于toolkit:

  • 支持typescript -- OK
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK,除了builder的链式使用方式
  • 原子性低问题 -- OK
  • 支持esmodule -- OK

recoil

recoil,react官方状态管理库,随着react17而来,官方网址为recoiljs.org,其实透过官方文档,我们可以看到差不多是完全遵循了react hooks的使用方式,不需要搭配任何连接器,可以与react直接无缝连接。不过这其实也导致了原子性比较强,统一的状态管理需要对其进行二次封装,而且工作量不小。在typescript方面,0.3.0开始支持,当前为止最新的版本是0.3.1。例子我就看下官方的例子

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

const textState = atom({
  key: 'textState', // unique ID (with respect to other atoms/selectors)
  default: '', // default value (aka initial value)
});

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

const charCountState = selector({
  key: 'charCountState', // unique ID (with respect to other atoms/selectors)
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

由上面,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK,虽然力度不是很大
  • 使用函数式编程 -- OK
  • 使用舒服 -- NO
  • 原子性低问题 -- NO
  • 支持esmodule -- OK

zustand

zustand,这个库,说实话,是第一次看到,不过看了npm上面的例子,这个库还是很好用&很实用的,使用起来很舒服,提供的api不是很多,但是够精简,能够满足需求。没有单独的官网,不过readme写的足够详细,算是个地址吧npm zustand, 我们来看下官网提供的例子:

import React from "react";
import create from "zustand";
import PrismCode from "react-prism";
import "prismjs";
import "prismjs/components/prism-jsx.min";
import "prismjs/themes/prism-okaidia.css";

const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time));
const code = `import create from 'zustand'

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

function Controls() {
  const inc = useStore(state => state.inc)
  return <button onClick={inc}>one up</button>
)

function Counter() {
  const count = useStore(state => state.count)
  return <h1>{count}</h1>  
}`;

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
  sleep: async () => {
    await sleep(2000);
    set((state) => ({ count: state.count + 30 }));
  }
}));

function Counter() {
  const { count, inc, sleep } = useStore();
  return (
    <div class="counter">
      <span>{count}</span>
      <button onClick={inc}>one up</button>
      <button onClick={sleep}>30 up</button>
    </div>
  );
}

export default function App() {
  return (
    <div class="main">
      <div class="code">
        <div class="code-container">
          <PrismCode className="language-jsx" children={code} />
          <Counter />
        </div>
      </div>
    </div>
  );
}

可以看到所有的数据使用的createStore进行包裹,里面可以定义任意类型,可以是count的这样的stats类型,也可以使用函数(包括异步函数),做到了最简单化;另外zustand还提供了一些其他的工具函数和中间件,关于中间件和工具函数等的如何使用,此处就不多说了,可以去npm看看.

由上面,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK, 但是官网描述里面的例子比较少
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK
  • 原子性低问题 -- 不高不低,中等,可能需要使用到中间件来包裹,扩展使用
  • 支持esmodule -- OK

rematch

rematch, 因为有部分项目在使用这个库,所以简单看了下使用。官网上面有很多例子,可以去看看: rematchjs.org. v1的时候是不支持typescript的,使用上有两种方式(对于effects)

// 方式一
effects: {
  fetchInfo: async () => {
    const res = await requestInfo();
    this.setState({
      ...res;
    })
  }
}
// 方式二
effects: (dispatch) => {
  return {
    fetchInfo: async () => {
      const res = await requestInfo();
      dispatch.info.setInfo(res);
    }
  }
}

v2的时候是增加了typescript的支持,不过却去掉了上面方式一的使用方式,只保留了第二种。具体例子可以前往rematch typescript 查看。这个使用方式其实与上面的redux-toolkit稍微有点相似,不过好像rematch最近下载量下降了不少。

rematch在模块化封装部分做的很好,能够对所有的状态进行统一管理,然后可以按models进行划分功能,使用起来比较舒服。

由上面以及官网上面的一些例子,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK
  • 原子性低问题 -- OK
  • 支持esmodule -- OK

concent

concent,另外一种状态管理器的实现方式,在使用上与Vue3的setup有很大相似之处。为什么会谈到concent,因为有好几个项目在使用concent,而且表现良好。官方网站concentjs.concent功能非常强大,各种黑魔法,要想使用好concent,会有比较大的学习成本,也就是使用起来可能不会很简单。

import { StrictMode } from "react";
import ReactDOM from "react-dom";
import { run, useConcent } from "concent";

const sleep = (time = 1000) => new Promise((r) => setTimeout(r, time));

run({
  counter: {
    state: {
      value: ""
    },
    computed: {},
    lifecycle: {},
    reducer: {
      decrement: async (payload, moduleState) => {
        await sleep(1000);
        return {
          value: moduleState.value - 1
        };
      }
    }
  }
});

const setup = (ctx) => {
  ctx.effect(() => {
    ctx.setState({
      value: 1000
    });
  }, []);
  const increment = () => {
    console.log(1233);
    ctx.setState({
      value: ctx.state.value + 1
    });
  };
  return {
    increment
  };
};

export default function App() {
  const { state, settings, moduleReducer } = useConcent({
    module: "counter",
    setup
  });
  return (
    <div className="App">
      <h1>Hello Counter : {state.value}</h1>
      <div>
        <button onClick={settings.increment}>click increment</button>
        <button onClick={moduleReducer.decrement}>click decrement</button>
      </div>
    </div>
  );
}

ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  document.getElementById("root")
);

完整的例子和api可以前往官网查看,支持class装饰器的模式,也支持函数setup的方式,也有不使用setup的方式,都能满足;问题就是api较多,学习成本较高。

根据经验和上面的例子,我们可以简单再对比下我们的原则:

  • 支持typescript -- OK
  • 使用函数式编程 -- OK
  • 使用舒服 -- OK,就是学习成本较高
  • 原子性低问题 -- OK,支持模块化,状态统一管理
  • 支持esmodule -- OK

结论

至此,这几种状态管理库的使用方式和原则对比差不多已经完成。可能有使用不当或者说明错误的地方,欢迎指出。

最终我们考虑了以下几点:

  • 支持typescript,使用函数式编程
  • 支持模块化,状态统一管理
  • 学习成本低,且状态管理库比较流行
  • 考虑大家的意见,这很客观,选择一个大家都愿意使用的库

最终选择的是redux官方的toolkit来进行统一的状态管理。

本文结束,感谢阅读,欢迎讨论交流。如有不当之处感谢指出。

关注

欢迎大家关注我的公众号[德莱问前端],文章首发在公众号上面。

除每日进行社区精选文章收集外,还会不定时分享技术文章干货。

希望可以一起学习,共同进步。

@dravenww dravenww reopened this Mar 21, 2024
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

1 participant