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 吧~~~