共计 6978 个字符,预计需要花费 18 分钟才能阅读完成。
React 是一个视图层框架,其核心思想是 UI = f(state),即「UI 是 state 的投影」,state 自上而下流动,整个 React 组件树由 state 驱动。当一个 React 应用程序足够简单,组件嵌套足够深时,组件树中的状态流动会变得难以管制(例如你如何跟踪父节点的 state 流动到叶子节点时产生的变动)。这时咱们就须要对 state 进行治理,在进行状态治理的同时,还须要分清 React 利用中有哪些状态类型,不便制订出最适宜的状态治理计划。
状态类型
React 应用程序状态从宏观意义上讲能够分为两类:
- 客户端状态 Client State:少数用于管制客户端的 UI 展现,如下文将介绍到的 Local state、Feature state、Application state 都属于客户端状态。
- 服务端状态 Server State:客户端通过异步申请取得的数据。
本地状态 – Local state
仅存在于单个组件中的状态,咱们能够称之为「本地」或「UI」状态。
通常它能够帮忙咱们治理用户界面交互,例如显示和暗藏内容或启用和禁用按钮,它还常常在咱们期待时管制渲染的内容。思考以下示例:
function Text() {const [viewMore, setViewMore] = useState(false); | |
return ( | |
<Fragment> | |
<p> | |
React makes it painless to create interactive UIs. | |
{ | |
viewMore && <span> | |
Design simple views for each state in your application. | |
</span> | |
} | |
</p> | |
<button onClick={() => setViewMore(true)}>read more</button> | |
</Fragment> | |
) | |
} |
viewMore
是一种仅对这个特定组件有意义的状态,它的作用是仅管制此处文本的可见性。
在此示例中,viewMore
对应用程序的其余组件对都是没用的,因而,您不用将此状态透露到 Text
组件之外。
特色状态 – Feature state
组合两个或多个组件,这些组件须要晓得雷同的信息,咱们将这种状态定义为「特色」状态。
能够说,每一个非本地性的状态都属于这一类。然而,并非每个特色状态都是雷同的,咱们将在本文中进一步理解这一点。
特色状态的一个很好的例子是表单状态,它看起来有点像下面形容的 UI 状态,但它联合了多个输出,治理多个组件。
import {useState} from "react"; | |
const Skill = ({onChange}) => ( | |
<label> | |
技能: | |
<input type="text" onChange={(e) => onChange(e.target.value)} /> | |
</label> | |
); | |
const Years = ({onChange}) => ( | |
<label> | |
工龄: | |
<input type="text" onChange={(e) => onChange(e.target.value)} /> | |
</label> | |
); | |
export default function Form() {const [skill, setSkill] = useState(""); | |
const [years, setYears] = useState(""); | |
const isFormReady = skill !== ""&& years !==""; | |
return (<form onSubmit={() => alert("提及胜利")}> | |
<Skill onChange={setSkill} /> <br /> | |
<Years onChange={setYears} /> | |
<button disabled={!isFormReady}>submit</button> | |
</form> | |
); | |
} |
这里咱们有一个 Form 表单,它蕴含两个字段 skill
和 years
,默认状况下,Form 表单的提交按钮处于禁用状态,仅当两个输出都有值时该按钮才变为启用状态,请留神 skill
和 years
都是必须的,以便咱们能够计算 isFormReady
的值。Form 是实现此类逻辑的最佳场合,因为它蕴含了所有相关联的元素。
此示例体现了特色状态和利用状态之间的一个界线,在这里也能够应用 Redux 进行状态治理,然而这不是一种好的做法, 咱们应该在它被晋升并成为应用程序状态之前更早地辨认特色状态 。
利用状态 – Application state
应用程序状态是疏导用户整体体验的状态,这可能是受权状态、配置文件数据或全局款式主题。 在应用程序的任何中央都可能须要它。上面是一个简略的示例:
import React, {useContext, useState} from "react"; | |
const ThemeContext = React.createContext(); | |
const Theme = ({onChange}) => {const { theme} = useContext(ThemeContext); | |
return `Theme: ${theme}`; | |
} | |
const ThemeSelector = () => {const { theme, toggleTheme} = useContext(ThemeContext); | |
return (<select value={theme} onChange={toggleTheme}> | |
<option value="light">light</option> | |
<option value="dark">dark</option> | |
</select> | |
); | |
} | |
export default function App() {const [theme, setTheme] = useState("light"); | |
const toggleTheme = () => setTheme(theme === "light" ? "dark" : "light"); | |
const themeStyle = {background: theme === "light" ? "#fff" : "#b9b9b9"}; | |
return (<ThemeContext.Provider value={{ theme, toggleTheme}}> | |
<div className="App"> | |
<header style={themeStyle}> | |
<Theme /> | |
</header> | |
<footer style={themeStyle}> | |
<ThemeSelector /> | |
</footer> | |
</div> | |
</ThemeContext.Provider> | |
); | |
} |
这个例子中,有一个页头和一个页脚,他们都须要晓得以后应用程序的主题。咱们通过应用 Context API 将 theme
设置为利用状态,这样做的目标以便于 Theme
和 ThemeSelector
也能轻松的拜访(它们也须要拜访 theme
,但它们有可能嵌套于其余组件之中)。
如果某些属性许多组件都须要,并且可能须要从近程组件进行更新,那么咱们可能必须将其设置为应用程序状态。
服务器状态 – Server state
服务器状态能够了解为接口状态,服务端状态有以下特点:存储在远端,本地无奈间接管制、须要异步 API 来查问和更新、可能在不知情的状况下,被另一个申请方更改了数据,导致数据不同步等。
现有的状态治理库(如 Mobx、Redux 等 ) 实用于治理客户端状态,但它们并不关怀客户端是如何异步申请远端数据的,所以他们并不适宜解决异步的、来自服务端的状态。
要辨认服务器状态,您必须思考数据更改的频率以及这些数据的起源。 如果它或多或少是动态的,那么咱们应该防止从服务端获取,只需将其存储到客户端并将其作为全局变量传递。
状态治理
随着 React 生态一直壮大,社区中提供了多种形式来解决状态治理,有基于 Flux 思维的 Redux、Zustand 以及 React 自带的 useReducer + Context;有基于原子化的 Recoil、Jotail 等;还有响应式计划的代表 Mobx,在这里不过多针对介绍,咱们来理解一些最罕用的。
Hooks 是状态治理的次要机制
useState
或 useReducer
是大多数人用来治理本地状态的形式,useState
实际上是 React 中从新渲染的次要机制,这也是为什么大多数状态治理库都在底层应用它的起因。如果深入研究,你会发现这些不同的第三方库提供的 Custom Hooks 都依赖于默认的 useState
、useReducer
和 useffect
,上面是官网提供的一个 useReducer
的简略示例:
const initialState = {count: 0}; | |
function reducer(state, action) {switch (action.type) { | |
case 'increment': | |
return {count: state.count + 1}; | |
case 'decrement': | |
return {count: state.count - 1}; | |
default: | |
throw new Error();} | |
} | |
function Counter() {const [state, dispatch] = useReducer(reducer, initialState); | |
return ( | |
<> | |
Count: {state.count} | |
<button onClick={() => dispatch({type: 'decrement'})}>-</button> | |
<button onClick={() => dispatch({type: 'increment'})}>+</button> | |
</> | |
); | |
} |
状态晋升 – Lifting State Up
在 React 中,将多个组件中须要共享的 state 向上挪动到它们的最近独特父组件中,便可实现共享 state,这就是所谓的「状态晋升」。
回顾一下本文结尾的 view-more
示例(本地状态来管制文本的可见性)。然而,如果有一个新的要求,咱们有一个全局的「开展文本」按钮,而后咱们必须把这个状态晋升,而后通过 props
传递上来。
// viewMore 是一个 local state | |
function Component() {const [viewMore, setViewMore] = useState(false); | |
return ( | |
<Fragment> | |
<p>Text... {viewMore && <span>More text ...</span>}</p> | |
<button onClick={() => setViewMore(true)}>read more</button> | |
</Fragment> | |
); | |
} | |
// 将 viewMore 晋升为组件的 feature state | |
function Component({viewMore}) { | |
return (<p>Text... { viewMore && <span>More text ...</span>}</p> | |
); | |
} |
当您看到一个部分状态变量变成一个 props
时,咱们就能够视其为状态晋升。 这种形式的须要留神在 prop drilling
方面的找到均衡,您也不心愿有许多只是负责将 props
传递给他们子组件的「中间人」组件。
应用 Context API
Context API 提供了一种通过组件树传递数据的办法,而无需在每个级别手动向下传递 props。
在典型的 React 应用程序中,数据通过 props 自上而下(父级到子级)传递,但这种做法对于某些类型的 props 而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都须要的。Context 提供了一种在组件之间共享此类值的形式,而不用显式地通过组件树的逐层传递 props。
Context 设计目标是为了共享那些对于一个组件树而言是「全局」的数据,例如以后认证的用户、主题或首选语言,因而 Context 次要利用于利用状态的治理(见利用状态示例)。
如果你只是想防止层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。
组合模式 – Composition mode
Context API 适宜治理利用状态,如果想防止 Props Drilling 等问题,能够采纳组合模式。组合是一种通过将各组件联结在一起以创立更大组件的形式,组合不仅具备多变的灵活性和可重用性,还具备繁多职责的个性,组合也是 React 的外围。看一个官网的示例:
function SplitPane(props) { | |
return ( | |
<div className="SplitPane"> | |
<div className="SplitPane-left"> | |
{props.left} | |
</div> | |
<div className="SplitPane-right"> | |
{props.right} | |
</div> | |
</div> | |
); | |
} | |
function App() { | |
return ( | |
<SplitPane | |
left={<Contacts />} | |
right={<Chat />} /> | |
); | |
} |
示例中没有应用 children
,而是自行约定将 Contacts
和 Chat
两个组件通过 props.left/props.right
传入,被传递的组件同父组件组成了更加简单的组件,尽管被组合在一起,但各个成员组件都具备繁多职责。
client-server 形式
这种形式体现在 Apollo 和 ReactQuery 中,基本上都是在应用程序中增加一个 layer/client
并由其负责从内部源申请 / 缓存数据。
它们带有一些不错的 hooks,对于客户端来说,这种形式看起来很像应用本地状态,它打消了数据存储和获取的繁杂性。上面是 Apollo 和 ReactQuery 的简略示例:
// Apollo | |
import {useQuery, gql} from '@apollo/client'; | |
// 在这个例子中,咱们应用了 GraphQL | |
const EXCHANGE_RATES = gql` | |
query GetExchangeRates {rates(currency: "USD") { | |
currency | |
rate | |
} | |
} | |
`; | |
function ExchangeRates() {const { loading, error, data} = useQuery(EXCHANGE_RATES); | |
if (loading) return <p>Loading...</p>; | |
if (error) return <p>Error :(</p>; | |
return data.rates.map(({currency, rate}) => (<div key={currency}> | |
<p> | |
{currency}: {rate} | |
</p> | |
</div> | |
)); | |
} |
// ReactQuery | |
import React from "react"; | |
import ReactDOM from "react-dom"; | |
import {QueryClient, QueryClientProvider, useQuery} from "react-query"; | |
const queryClient = new QueryClient(); | |
export default function App() { | |
return (<QueryClientProvider client={queryClient}> | |
<Example /> | |
</QueryClientProvider> | |
); | |
} | |
function Example() {const { isLoading, error, data, isFetching} = useQuery("repoData", () => | |
fetch("https://api.github.com/repos/tannerlinsley/react-query").then((res) => res.json()) | |
); | |
if (isLoading) return "Loading..."; | |
if (error) return "An error has occurred:" + error.message; | |
return ( | |
<div> | |
<h1>{data.name}</h1> | |
<p>{data.description}</p> | |
<strong>👀 {data.subscribers_count}</strong>{" "} | |
<strong>✨ {data.stargazers_count}</strong>{" "} | |
<strong>🍴 {data.forks_count}</strong> | |
<div>{isFetching ? "Updating..." : ""}</div> | |
</div> | |
); | |
} | |
const rootElement = document.getElementById("root"); | |
ReactDOM.render(<App />, rootElement); |
从例子中咱们发现,前端负责指定如何获取数据,其余的都取决于 Apollo/ReactQuery 客户端,包含前端须要的 loading
和一个 error
两种状态都由后端提供并治理。这使得前端领有一个状态,但实际上容许在后端治理该状态,是一种乏味的联合。
论断
状态治理很简单,状态治理没有最好的计划,只有最合适的计划。
对于第三方状态治理库举荐浏览 56 个 NPM 包解决 16 个 React 问题 里的大节。
参考:
https://krasimirtsonev.com/bl…
https://www.sytone.me/b-syste…