你可能没那么需要 useState
原文发表在我的博客 doma.land/blog/you-ma…
useState 是我们在编写 React 应用中最常用的 hook 之一。但是,你对它的需求也许并没有你想象的那么多。
在这篇文章中,我们将探讨在何种情况下可以使用其他方法替代 useState
。
状态的几种类型
在 React 应用中,根据形态的不同,我将状态大致分为如下几类:
远程状态
这是一类特别的状态,我对“远程状态”这一概念的认识从接触 swr
和 react-query
库开始。
远程状态指的是,其源头其实不在你的本地应用中,而是在远程服务上。你对此状态的读取和修改,最终都反映在存储在远程服务上的一段信息(也即 REST 中的“资源”)的变化。事实上,我们所开发的大部分 BS 架构的应用的前端部分,其核心业务逻辑就是在读取和修改远程状态(通过服务端提供的 API)。
function App() {
// todos 其实并不是由本地应用声明和维护的值
// 而是服务端上一段信息的副本
const [todos, setTodos] = useState([])
useEffect(() => {
api.fetchTodos().then(data => setTodos(data))
}, [])
return (
<TodoList
todos={todos}
onCreateTodo={data => api.createTodo(data)}
onEditTodo={(todo, data) => api.updateTodo(todo.id, data)}
onDeleteTodo={todo => api.deleteTodo(todo.id)}
/>
)
}
远程状态其实不需要你通过 useState
来声明。也许你以前这样做(配合 useEffect
来更新),但现在我推荐你使用诸如 swr
或 react-query
这样的库,它们提供了直观的声明式 API,并且把请求的刷新、去重、轮询这些关于“如何适时地将远程状态与本地副本进行同步”的问题处理得很好。
function App() {
const { data: todos } = useQuery("todos", api.fetchTodos)
return (
<TodoList
todos={todos}
onCreateTodo={data => api.createTodo(data)}
onEditTodo={(todo, data) => api.updateTodo(todo.id, data)}
onDeleteTodo={todo => api.deleteTodo(todo.id)}
/>
)
}
即便你在开发仅有一组 CRUD 接口的小型应用,我也会建议你使用它们,而不是自己在 useEffect
中更新状态(那样你还得处理组件卸载时的 Promise 回调等问题)。
除了远程状态,剩下的就是本地状态。本地状态可以进一步细分为以下几类:
表单状态
从前文提到的“前端应用的核心业务逻辑就是在读取和修改远程状态”这个角度来看,表单正是修改远程状态的前一步,为修改远程状态提供输入(有时还代其进行一些校验)。因此,表单是前端应用中非常重要的组成部分。
表单状态的结构和操作逻辑大体上往往都是相同的——结构上,它包含表单项的值、表单项的错误信息(如果有校验);操作逻辑上,包含表单项值的更新、错误信息的更新(表单校验)等等。因此,社区将这些通用的逻辑封装成了库,例如 formik
和 react-hook-form
,它们提供以 hook 的形式管理表单状态的 API。如果你准备创建比较健壮的表单,我建议你使用这些库。
const {
values,
errors,
setFieldValue,
setFieldError,
handleSubmit,
isSubmitting,
} = useFormik({
initialValues: {
firstName: "",
lastName: "",
email: "",
},
onSubmit: values => {
alert(JSON.stringify(values, null, 2))
},
})
另外,我并不建议使用那些,声称可以帮你少写代码,而将表单状态逻辑和表单渲染绑定在一起的库,我坚持认为逻辑和表现最好不要耦合。
视觉表现状态
除了表单状态,剩下的本地状态就是控制视觉表现的状态了。例如,侧边导航菜单是否展开/折叠、当前选中哪个 Tab 页/列表项,等等。 大多数时候,它们的值是基本类型,且更新逻辑非常简单。
const [isExpanded, setExpanded] = useState(false)
const [selectedTab, setSelectedTab] = useState("blog")
我仅在这些时候才使用 useState
,在一个 70,000+ 行的应用中,我仅仅使用了约 20 个 useState
。
有些时候,这些状态的类型和更新逻辑并没有那么简单。我建议你使用自定义 hook 或者 reducer 将这些状态和更新的逻辑封装起来,提高可预测性和可测试性。本文不展开讨论。
合并多个相关的状态
在某些场景下,如果你发现在同一个组件中连续使用了多个 useState
(且它们都是实现这个组件所必要的),并且经常在同一个回调中同时更新多个 state,那么 Mutable API 可能很适合你。诸如 use-immer
、use-mutative
等提供 Mutable API 的库,允许你以可变的方式更新 state。例如:
const [state, setState] = useMutative({
foo: "bar",
list: [{ text: "todo" }],
})
return (
<button
onClick={() => {
// set value with draft mutable
setState(draft => {
draft.foo = `${draft.foo} 2`
draft.list.push({ text: "todo 2" })
})
}}
>
click
</button>
)
除了 API 风格的偏好之外,Mutable API 更有用的地方在于,使我们可以将多个相关的状态结合在一起进行声明和更新。 例如,我们需要实现这样一个列表组件:
- 它的列表项可以被选中,同时最多有一个选中的列表项
- 当新增一个列表项时,选中新增的列表项
我们可能需要这样两个状态:
const [items, setItems] = useState([{ id: 1 }])
const [selectedItemKey, setSelectedItemKey] = useState(1)
return (
<button
onClick={() => {
setItems(items => [...items, { id: 2 }])
setSelectedItemKey(2)
}}
>
新增
</button>
)
如果使用 Mutable API,则写法如下:
const [state, setState] = useMutative({
items: [{ id: 1 }],
selectedItemKey: 1,
})
return (
<button
onClick={() => {
setState(state => {
state.items.push({ id: 2 })
state.selectedItemKey = 2
})
}}
>
新增
</button>
)
看起来是不是更直观了?并且,这样写只会进行一次状态更新(不过这并不关键,因为新版本的 React 本身就会将多个连续的状态更新合并以避免多次重渲染)。
总结
我想要通过本文建议的是,尽可能地将散落在各个组件内部的状态,使用自定义 hook,甚至现成的库来代替。 这有助于降低逻辑和表现的耦合,提高可复用性和可测试性,并且长期来看,提高可维护性。
当你回顾六个月前的自己编写的代码,如果遇到因为错综的状态更新逻辑而感到困惑的情况,不妨尝试文中的方法来做出改变。
转载自:https://juejin.cn/post/7212548256944111674