HooX 是一个基于 hook 的轻量级的 React 状态管理工具。使用它可方便的管理 React 应用的全局状态,概念简单,完美支持 TS。
1. 更拥抱函数式组件
从 React@16.8 的 hook 到 vue@3 的composition-api,基本可以断定,函数式组件是未来趋势。HooX提供了函数式组件下的状态管理方案,以及完全基于函数式写法的一系列 API,让用户更加的拥抱函数式组件,走向未来更进一步。
2. 简化纯 hook 写法带来的繁杂代码
写过 hook 的同学肯定知道,hook 带来的逻辑抽象能力,让我们的代码变得更有条件。但是:
- useCallback/useMemo 真的是写的非常非常多
- 由于作用域问题,一些方法内的 state 经常不知道到底对不对
实际举个例子吧,比如一个列表,点击加载下一页,如果纯 hook 书写,会怎么样呢?
import { useState, useEffect } from 'react'const fetchList = (...args) => fetch('./list-data', ...args)export default function SomeList() { const [list, setList] = useState([]) const [pageNav, setPageNav] = useState({ page: 1, size: 10 }) const { page, size } = pageNav // 初始化请求 useEffect(() => { fetchList(pageNav).then(data => { setList(data) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // 获取下一页内容 const nextPage = () => { const newPageNav = { page: page + 1, size } fetchList(newPageNav).then(data => { setList(data) setPageNav(newPageNav) }) } return ( <div> <div className="list"> {list.map((item, key) => ( <div className="item" key={key}> ... </div> ))} </div> <div className="nav"> {page}/{size} <div className="next" onClick={nextPage}> 下一页 </div> </div> </div> )}
很常规的操作。现在,我希望给“下一页”这个方法,加个防抖,那应该怎么样呢?是这样吗?
// 获取下一页内容const nextPage = debounce(() => { const newPageNav = { page: page + 1, size } fetchList(newPageNav).then(data => { setList(data) setPageNav(newPageNav) })}, 1000)
乍一看好像没有问题。但其实是有很大隐患的!因为每次 render 都会带来一个全新的 nextPage
。如果在这1秒钟内,组件因为状态或props的变更等原因导致重新 render,那这时再点击触发的已经是一个全新的方法,根本起不到防抖的效果。那该怎么做?必须配合 useMemo
,然后代码就会变成这样:
// 获取下一页内容const nextPage = useMemo( () => debounce(() => { const newPageNav = { page: page + 1, size } fetchList(newPageNav).then(data => { setList(data) setPageNav(newPageNav) }) }, 1000), [page, size])
nextPage
内部依赖于 page/size
,所以 useMemo
第二个参数必须加上他们。不过依旧不够爽的是,每当我的 page
跟 size
变化时, nextPage
依旧会重新生成。
难受。
还有一个问题,由于使用 list
跟 pageNav
使用了两个 useState
,每次更新都会带来一次渲染。如果我们合成一个 state
,由于 useState
返回的 setState
是重置状态,每次都要传递全量数据,比如这样:
const [{ list, pageNav }, setState] = useState({ list: [], pageNav: { page: 1, size: 10 }})const { page, size } = pageNav// 初始化请求useEffect(() => { fetchList(pageNav).then(data => { setState({ list: data, pageNav }) }) // eslint-disable-next-line react-hooks/exhaustive-deps}, [])
毫无意义的设置了一次 pageNav
,难受。更复杂一点儿的场景可以采用传递函数,获取旧数据,稍微缓解一点,但是也还是挺麻烦的。
当然啦,市面上也有一些库可以解决合并更新的问题,比如 react-use
的useSetState。
不过,这里提供了另外一种,一劳永逸的解决方案---HooX。
下面我们上HooX来实现这个逻辑。
import { useEffect } from 'react'import createHoox from 'hooxjs'import { debounce } from 'lodash'const fetchList = (...args) => fetch('./list-data', ...args)const { useHoox, getHoox } = createHoox({ list: [], pageNav: { page: 1, size: 10 }})const nextPage = debounce(() => { const [{ pageNav }, setHoox] = getHoox() const newPageNav = { page: pageNav.page + 1, size: pageNav.size } fetchList(newPageNav).then(data => { setHoox({ list: data, pageNav: newPageNav }) })})const initData = () => { const [{ pageNav }, setHoox] = getHoox() fetchList(pageNav).then(data => { setHoox({ list: data }) })}export default function SomeList() { const [{ list, pageNav }] = useHoox() const { page, size } = pageNav // 初始化请求 useEffect(initData, []) return ( <div> <div className="list"> {list.map((item, key) => ( <div className="item" key={key}> ... </div> ))} </div> <div className="nav"> {page}/{size} <div className="next" onClick={nextPage}> 下一页 </div> </div> </div> )}
由于我们把这些更新状态的操作都抽离出组件跟 hook 内部了,再加上 getHoox
能获取最新的数据状态,自然就没了 useMemo
,也不需要传递什么依赖项。当我们的场景越来越复杂,函数/数据在组件间的传递越来越多时,这块的受益就越来越大。这是某些将 hook 全局化的状态管理器没有的优势。
3. 完美的 TS 支持及编辑器提示
由于 HooX 完全采用 ts 实现,可以完美的支持 TS 和类型推导。拿上面的代码举例:
另外由于每个 action/effect
都是单独声明,直接引用。所以无论他们定义在哪里,编辑器都能直接定位到相应地实现,而不需要像 dva
那样借助于 vscode 插件。
4. 可兼容 class 组件
对于一些历史上的 class 组件,又想用全局状态,又不想改造怎么办?也是有办法的,hoox 提供了 connect
,可以方便的将状态注入到 class 组件中。同样的举个例子(TS):
class SomeList extends React.PureComponent<{ list: any[] pageNav: { page: number; size: number } nextPage: () => void initData: () => void}> { componentDidMount() { this.props.initData(); } render() { const { list, pageNav, nextPage } = this.props; const { page, size } = pageNav; return ( <div> <div className="list"> {list.map((item, key) => ( <div className="item" key={key}> ... </div> ))} </div> <div className="nav"> {page}/{size} <div className="next" onClick={nextPage}> 下一页 </div> </div> </div> ); }}const ConnectedSomeList = connect(state => ({ list: state.list, pageNav: state.pageNav, nextPage, initData,}))(SomeList);
由于所有的 props
都被注入了,因此最终返回的新组件,不需要任何 props 了(因此为 never)。当然也可以选择注入部分 props。
由于只注入了 list
跟 pageNav
,因此 nextPage
跟 initData
依旧会成为组件需要的 props。
5. 实现简单,没有 Hack,稳定
HooX的实现非常简单,去除一些类型推导,约 100 行代码,完全基于 Context
+ Provider
,无任何黑科技,纯 react 原生机制与能力,所以不用担心会出现一些奇奇怪怪的问题。
如果自己有什么特殊诉求,完全也可以自己 fork 一份自行维护,维护成本极低。
说了这么多,快来试试HooX吧~~~