最近接手公司的一个较新项目,其状态管理使用了Facebook开发的一个状态管理库Recoil,在吃了一个月老项目使用的redux的石第一次接触Recoil后,发现真的好用,同事吃饭也怒喷redux举推举Recoil。遂花点时间学习并撰写文章和大家讲讲Recoil的基本使用和好处
通过本篇文章大家会学习到:
- 如何使用Recoil
- 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也是一个好的选择。