🤭同事说Redux太恶心了,换了Recoil是真香🍰

1,857 阅读8分钟

最近接手公司的一个较新项目,其状态管理使用了Facebook开发的一个状态管理库Recoil,在吃了一个月老项目使用的redux的石第一次接触Recoil后,发现真的好用,同事吃饭也怒喷redux举推举Recoil。遂花点时间学习并撰写文章和大家讲讲Recoil的基本使用和好处

通过本篇文章大家会学习到:

  1. 如何使用Recoil
  2. Recoil优势在哪?

如何使用Recoil

Recoil是开发的第三方库,所以我们想要用它首先就得把包下载下来

npm:

npm install recoil

yarn:

yarn add recoil

如需在组件中使用 Recoil,则可以将 RecoilRoot 放置在父组件的某个位置。将他设为根组件为最佳:

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

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

Recoil的核心概念

截一句官方文档的一句话

使用 Recoil 会为你创建一个数据流向图,从 atom(共享状态)到 selector(纯函数),再流向 React 组件。Atom 是组件可以订阅的 state 单位。selector 可以同步或异步改变此 state。

什么意思呢?

  • 组件通过 hook(如 useRecoilValue())订阅某个 atom 或 selector

  • atom 是状态源,值改变后会触发依赖更新

  • 如果组件使用的是 selector,Recoil 会先重新计算 selector(同步或异步)

  • 组件根据变化重新渲染(但只渲染用到的部分)

  • Recoil 内部维护一棵状态依赖树,知道谁依赖谁,只做精准更新

Recoil像我们React自带的useState一样,当某个组件订阅了atom或者是selector的值,这些值被改变后,Recoil内部会有一颗状态依赖树,只会精准更新这些依赖了atom或selector的的组件且会重新渲染依赖的组件。

所以我们想要了解Recoil,就不得不学习atom和selector是什么


Atom的基本知识

Atom是Recoil原子化特性的重要api,是状态的单位。它们可更新也可订阅:当 atom 被更新,每个被订阅的组件都将使用新值进行重渲染。它们也可以在运行时创建。可以使用 atom 替代组件内部的 state。如果多个组件使用相同的 atom,则这些组件共享 atom 的状态

创建Atom

const fontSizeState = atom({
  key: 'fontSizeState',
  default: 14,
});

其中Key,它是 Recoil 用来区分不同状态、缓存 selector 计算、调试时显示状态名的重要依据,所以这个key不能够重复,而default是我们存储想要的值的地方。

我们在这里定义了数据,放到组件该如何去使用呢?其使用api与hook类似:

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return (
    <button onClick={() => setFontSize((size) => size + 1)} style={{fontSize}}>
      Click to Enlarge
    </button>
  );
}

这个时候相信读者就会有一种既视感:这个操作怎么和useState那么相似?

这便是Recoil的第一个好处:与React高度集成,完全基于Hook,如useRecoilState。useRecoilValue。学过React的基本都会用,学习难度低,易上手

但是比直接使用state的好处就是,他可以在组件间使用这个状态,

就比如上图:当我们点击这个按钮时,他的size就会+1,那么在下图使用的同样的状态也会跟着改变。 且这个状态更新,只会让依赖这个状态的组件重新渲染,不会影响别的组件,减少很多不必要的渲染导致的性能消耗

function Text() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  return <p style={{fontSize}}>This text will increase in size too.</p>;
}

Selector

官方文档是这么说的:

selector 是一个纯函数入参为 atom 或者其他 selector。当上游 atom 或 selector 更新时,将重新执行 selector 函数。组件可以像 atom 一样订阅 selector,当 selector 发生变化时,重新渲染相关组件。

Selector 被用于计算基于 state 的派生数据。这使得我们避免了冗余 state,通常无需使用reduce来保持状态同步性和有效性。作为替代,将最小粒度的状态存储在 atom 中,而其它所有内容根据最小粒度的状态进行有效计算。由于 selector 会追踪需要哪些组件使用了相关的状态,因此它们使这种方式更加有效。

当我们某些组件想要用某个被处理过后的状态,且这个组件的更新依赖于某个状态, 我们就可以使用selector将状态进行处理,在return出来使用,就比如下面的状态,其需要在前面设置的状态+px。我们就可以使用selector,让它对我们的fontSize状态进行处理,

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState',
  get: ({get}) => {
    const fontSize = get(fontSizeState);
    const unit = 'px';

    return `${fontSize}${unit}`;
  },
});

get 属性是用于计算的函数。它可以使用名为 get 的入参来访问 atom 以及其他 selector 的值。每当它访问另一个 atom 或 selector 时,就会创建相应的依赖关系,以便在更新另一个 atom 或 selector 时,该 atom 或 selector 也会被重新计算。

也就是说我们使用了get去获取我们想要的atom状态,就会自动与这个atom建立依赖,当这个atom被修改,不仅依赖atom的组件会改变,这个selector也会改变,同样,依赖这个selector的组件也会发生改变。

上图我们使用get获取且依赖了fontSizeState这个atom后并进行处理:+px之后再到组件中进行使用

function FontButton() {
  const [fontSize, setFontSize] = useRecoilState(fontSizeState);
  const fontSizeLabel = useRecoilValue(fontSizeLabelState);

  return (
    <>
      <div>Current font size: {fontSizeLabel}</div>

      <button onClick={() => setFontSize(fontSize + 1)} style={{fontSize}}>
        Click to Enlarge
      </button>
    </>
  );
}

当我们点击按钮,不仅fontsize改变,依赖它的fontSizeLabel也会改变

ps:useRecoilValue()为只读api,只用来读取状态不是改变状态



Recoil的优势

Recoil最重要的特性便是状态原子化,派生化。可以让我们将状态拆成一个一个小原子,每个组件可只订阅自己用的状态。举个例子: 我们有这样的一个文件结构: 需要实现这个需求:一个页面里有多个组件共享“用户名”

<App>
  ├── Header (显示用户名)
  ├── Sidebar (显示用户名)
  └── MainForm (编辑用户名)

Reciol实现:

// 共享状态
const usernameAtom = atom({
  key: 'username',
  default: '',
})

// 在 Header 中读取
const username = useRecoilValue(usernameAtom)

// 在 MainForm 中修改
const [username, setUsername] = useRecoilState(usernameAtom)

redux实现:

// reducer
const reducer = (state = { username: '' }, action) => {
  switch (action.type) {
    case 'SET_USERNAME':
      return { ...state, username: action.payload }
    default:
      return state
  }
}

// dispatch 修改
dispatch({ type: 'SET_USERNAME', payload: 'Tom' })

// useSelector 读取
const username = useSelector(state => state.username)

context实现:

// context
const UserContext = createContext()

const App = () => {
  const [username, setUsername] = useState('')
  return (
    <UserContext.Provider value={{ username, setUsername }}>
      ...
    </UserContext.Provider>
  )
}

// 使用
const { username } = useContext(UserContext)

通过Redux和Context的对比,我们能发现Recoil的优势和其他的痛点:

Reocil优点:

1.每个组件只订阅自己需要的状态,更新精准、性能好

2.无需 context 包裹无需 reducer无需 action,直接使用即可

3.状态粒度细,可拆可组合,扩展性强

Redux的缺点

1.要写 reducer、action、store,冗长

2.对于小型状态管理场景有点重

3.不容易拆分粒度更细的状态

不仅如此

Recoil支持异步获取状态

// recoilState.ts
import { atom, selector } from 'recoil'

export const userIdState = atom({
  key: 'userId',
  default: 1
})

export const userInfoSelector = selector({
  key: 'userInfo',
  get: async ({ get }) => {
    const userId = get(userIdState)
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
    return await res.json()
  }
})
// App.tsx
import { useRecoilValueLoadable, useRecoilState } from 'recoil'
import { userIdState, userInfoSelector } from './recoilState'

export default function RecoilApp() {
  const [id, setId] = useRecoilState(userIdState)
  const user = useRecoilValueLoadable(userInfoSelector)

  return (
    <div>
      <input type="number" value={id} onChange={e => setId(Number(e.target.value))} />
      {user.state === 'loading' && <p>加载中...</p>}
      {user.state === 'hasValue' && <p>{user.contents.name}</p>}
    </div>
  )
}

Redux:需要配合中间件(如 redux-thunk)且错误处理还是手动处理,繁琐冗长

// actions.js
export const fetchUser = (id) => async (dispatch) => {
  dispatch({ type: 'LOADING' })
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
  const data = await res.json()
  dispatch({ type: 'SUCCESS', payload: data })
}
// reducer.js
const init = { user: null, loading: false }
export const userReducer = (s = init, a) => {
  switch (a.type) {
    case 'LOADING': return { ...s, loading: true }
    case 'SUCCESS': return { user: a.payload, loading: false }
    default: return s
  }
}

// App.tsx
const App = () => {
  const dispatch = useDispatch()
  const { user, loading } = useSelector(s => s.user)

  return (
    <div>
      <button onClick={() => dispatch(fetchUser(1))}>加载</button>
      {loading ? <p>加载中...</p> : user && <p>{user.name}</p>}
    </div>
  )
}

Context:自己写useEffect且错误处理还是手动处理,非常麻烦

// UserContext.tsx
const UserContext = createContext()
export const UserProvider = ({ children }) => {
  const [user, setUser] = useState(null)

  const fetchUser = async (id) => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`)
    setUser(await res.json())
  }

  return (
    <UserContext.Provider value={{ user, fetchUser }}>
      {children}
    </UserContext.Provider>
  )
}
// App.tsx
const { user, fetchUser } = useContext(UserContext)
return (
  <div>
    <button onClick={() => fetchUser(1)}>加载</button>
    {user && <p>{user.name}</p>}
  </div>
)

当然Recoil还有很多API方便我们使用,感兴趣的可以阅读他们的官方文档: Recoil官方文档


总结

Recoil有什么特性?

原子化状态:每个atom 就是一个独立的状态单位,可以被多个组件共享和订阅,修改某个 atom,不会导致无关组件重新渲染。

派生状态:Selector 可基于 atom 或其他 selector 派生出新状态,支持同步和异步逻辑。实现逻辑分离、状态计算与缓存。精细化依赖追踪,只有依赖发生变化的组件才会重新渲染,提升性能。

与 React 深度集成:使用方式与 React Hooks 一致:useRecoilState、useRecoilValue、useSetRecoilState 等。上手容易,语法直观

异步状态处理天然内建:Selector 支持异步,避免手动写 effect、loading、error 等状态逻辑。

对比之下,Recoil有什么优势

特性Recoil 优势Redux 缺点Context 缺点
学习成本使用 Hooks 风格,快速上手学习曲线陡峭,中间件多逻辑全部自写
模块解耦状态分散在 atom 中,模块化所有状态集中管理组件嵌套层级深
异步处理selector 原生支持 async需 thunk、saga 等中间件手动处理 loading、error
性能优化自动追踪依赖,避免冗余更新需手动使用 reselect一改全刷,渲染不精细
状态组合selector 可组合多个状态状态组合繁琐不适合多个状态合并

如果公司这边有成熟的redux架构,就可以不考虑再使用Recoil加大负担,但是如果是在构建新项目,使用Recoil也是一个好的选择。