原文地址:Optimizing state management in React applications
原文作者:Krasimir Tsonev
译文出自:FE-star/speed
译者:shenxiang11
校对者:[]
本文链接:[]
构建用户界面一直是一个挑战。要解决的主要问题之一是状态管理。React应用程序也不例外。当Facebook公布这个库时,我们认为它是一个视图层。之后很快,我们意识到,它不仅仅是这样。凭借其单向的数据流,React让前端开发变得更加可预测和稳健。然而,一个重要问题的答案却没有找到--"我们如何管理状态?"。多年来,Facebook和React社区提出了不同类型的解决方案。在这篇文章中,我们将看到其中的一些,并将给出何时使用它们的提示。
状态管理是困难的,如果我们在错误的地方进行管理,就会变得更加困难。第一步是确定我们正在处理的是哪种状态。有不同的类型,弄清楚我们有哪种类型对于之后的工具设计至关重要。如果我们在这一点上做出错误的判断,我们最终会得到一个不是最佳的设置。因此,回答 "什么住在哪里?"这个问题很重要。
一个只存在于单个组件中的状态,我们认为是局部的。这方面的一个例子是 "查看更多 "功能。想象一下,我们有一个很长的文本,但我们把它截断,并在最后显示一个小链接。如果用户点击它,我们就会显示出内容。我们需要一个标志来告诉我们这个链接是否被按下。
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>
);
}
useState
在这个组件中的用法是我们的状态管理。识别这种类型的状态的技巧是问谁需要知道它。在这种情况下,它只是我们的 <Text>
组件。所以,在我们要消费这个值的地方创建 viewMore
标志是有意义的。
当我们试图管理太多的东西时,本地状态的问题经常出现。我们正确地定位了状态,但逻辑需要多个变量。请看下面的例子:
function ILoveCats() {
const [cat, setCatURL] = useState(null);
const [isLoading, setLoadingFlag] = useState(false);
const [error, setError] = useState(null);
async function getCat() {
setLoadingFlag(true);
try {
const query = await fetch('https://api.thecatapi.com/v1/images/search');
const [{ url }] = await query.json();
setLoadingFlag(false);
setCatURL(url);
} catch(error) {
setError(true)
}
}
return (
<>
<button onClick={getCat}>I love cats</button>
{ error && 'Meow! Meow!'}
{ isLoading && 'Loading ...'}
{ cat && <img src={cat} /> }
</>
)
}
<ILoveCats>
组件提供了一个功能,在屏幕上显示一张猫的图片。用户需要按下一个按钮,getCat
函数就会提供图片的 URL。注意我们有三个变量来管理整个过程。我们有一个保持 URL 的变量,另一个告诉我们请求是否在进行中,还有一个用于处理错误。我们可以想到一些其他的边缘情况,而且我们可能会带来更多的 useState
语句。在这种情况下,其中一个可能的解决方案是看一个状态机。
const STATES = {
IDLE: 'IDLE',
LOADING: 'LOADING',
SUCCESS: 'SUCCESS',
ERROR: 'ERROR'
}
function ILoveCats() {
const [status, setStatus] = useState({ value: STATES.IDLE });
async function getCat() {
setStatus({ value: STATES.LOADING });
try {
const query = await fetch('https://api.thecatapi.com/v1/images/search');
const [{ url }] = await query.json();
setStatus({ value: STATES.SUCCESS, url });
} catch(error) {
setStatus({ value: STATES.ERROR });
}
}
return (
<>
<button onClick={getCat}>I love cats</button>
{ status.value === STATES.ERROR && 'Meow! Meow!'}
{ status.value === STATES.LOADING && 'Loading ...'}
{ status.value === STATES.SUCCESS && <img src={status.url} /> }
</>
)
}
在这个新的组件版本中,我们只有一个变量 —— status
。它保留了用户当前的进度。我们可以谈很多关于状态机和它们的好处。如果你想探索这个概念,一定要看看 XState。我们必须澄清,我们并没有实现一个完整的状态机。我们没有过渡的定义。但即使没有这些,这个组件的阅读效果也会更好,而且变得更容易遵循逻辑。
我们将谈论的下一种类型的状态结合了多个组件。
特征状态是指我们有两个或多个组件共享相同的信息。这种状态的一个例子是每个包含多个输入的表单。让我们用一个例子来说明:
const Skill = ({ onChange }) => (
<label> Skill:
<input type="text" onChange={e => onChange(e.target.value)}/>
</label>
);
const Years = ({ onChange }) => (
<label> Years of experience:
<input type="text" onChange={e => onChange(e.target.value)}/>
</label>
);
function Form() {
const [skill, setSkill] = useState('');
const [years, setYears] = useState('');
const isFormReady = skill !== '' && years !== '';
return (
<form>
<Skill onChange={setSkill} />
<Years onChange={setYears} />
<button disabled={!isFormReady}>submit</button>
</form>
);
}
一个有两个文本输入的表单。用户在两个字段中都输入数值。我们默认禁用该按钮,只有在有两个值的情况下才启用。请注意,我们需要 skillls
和 years
在一起,这样我们才能计算出 isFormReady
。<Form>
组件是实现这种逻辑的完美场所,因为它封装了所有的输入。通常这样的组件会管理一个包含所有表单信息的数据对象。这个例子显示了功能和应用状态之间的细微差别(我们将在下一节看到)。我们可以很容易地将该状态放在 Redux 存储中。我们可以写一个 selector isFormReady
,创建 actions 和一个 reducer。这将工作得很好,但问题是这是否是这种信息的正确位置。在大多数情况下,答案是 "不"。
应用程序的状态是决定用户的整体体验的状态。它可能是授权状态、个人资料数据或全局主题。这样的状态有可能在应用中的任何地方都需要。这就是为什么上一节的表单不属于这个类别。用户输入通常被限定在特定的功能范围内。我们在那里需要的状态在交互结束后就会消失。应用状态是另一个需要在整个用户旅程中保持活力的数据。这里有一个简单的例子:
const ThemeContext = React.createContext();
const Theme = () => {
const { theme } = useContext(ThemeContext);
return <div style={{
background: theme === 'light' ? 'white' : 'grey'
}}>Hello world</div>;
}
const ThemeSelector = () => {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<select value={theme} onChange={toggleTheme}>
<option value="light">light</option>
<option value="dark">dark</option>
</select>
);
}
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
<header>
<Theme />
</header>
<footer>
<ThemeSelector />
</footer>
</ThemeContext.Provider>
);
}
这里我们有一个页眉和一个页脚。它们都需要知道应用程序的当前主题。这个例子很简单,但想象一下,Theme
和 ThemeSelector
是深深嵌套在其他组件中的组件。它们不能直接访问 theme
变量。它们也将其用于不同的事情。Theme
只显示当前值,而 ThemeSelector
则改变它。
我们可以比功能状态更快地识别这样的状态。如果某个状态被广泛使用,并且需要从远处的组件中更新,那么我们可能要把它放到应用状态中。这也是对大多数开发者来说最诱人的行动。我们首先考虑使用特征状态甚至是本地状态。这将使我们的生活更轻松。
最后,还有一种类型的状态不在浏览器中存在。这就是我们使用一些持久性存储的时候。比如说数据库。在过去的几年里,我们看到越来越多的工具朝着这个方向发展。例如,GraphQL,有时被用作应用程序状态的存储。所有的数据都存在于应用程序之外,而像 Apollo 这样的工具则处理数据的获取和同步。ReactQuery 是这方面的另一个例子。
从实现上看,状态的管理被抽象为一个钩子或一个组件。我们使用它,并没有过多地考虑数据的生命周期。下面是一个使用 ReactQuery 的例子。
import { QueryClient, QueryClientProvider, useMutation, useQuery } from 'react-query'
const queryClient = new QueryClient()
function Example() {
const { isLoading, error, data } = useQuery('my-data', loadData, { initialData: '' });
const { mutate } = useMutation(saveData, {
onSuccess: () => {
queryClient.invalidateQueries('my-data')
},
});
return (
<div>
<p>{data}</p>
<input type="text" onChange={e => mutate(e.target.value) } defaultValue={data}/>
</div>
)
}
function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
ReactDOM.render(<App />, document.querySelector('#root'));
async function saveData(value) {
return new Promise(done => {
setTimeout(() => {
localStorage.setItem('my-data', value);
done();
}, 1000);
})
}
async function loadData() {
return new Promise(done => {
setTimeout(() => { done(localStorage.getItem('my-data')); }, 1000);
});
}
saveData
和 loadData
是用来模拟数据库的。我们将 localStorage
的 API 包装在 promises 中,它是同步的,以使它看起来需要一些时间。我们将假装查询一个远程数据库。<Example>
组件使用 useQuery
和 useMutation
来加载和保存数据。有一种技术可以实现乐观的更新,这将使我们的演示变得优雅简洁。它将看起来像我们用 useState
做的。
当然,在这个例子中,我们只是触及到了表面。像 ReactQuery 这样的库可以做得更多。数据缓存、数据失效、错误处理、回退逻辑,等等。各种各样的事情,我们通常自己做。
状态管理的优化始于确定我们有什么样的状态。然后是选择合适的工具来完成这项工作。现在,React 作为一个库提供了许多管理状态的工具。Hooks 和 context API 对于大多数情况来说已经足够好了。如果它们不适合,那么我们有各种第三方工具供我们使用。例如,Redux 和 Redux toolkit,仍然能够使用。像 ReactQuery 这样的库步入了试图在生态系统中找到自己的位置的阶段。在工具方面,我们有很多不同的选择。最后我们应该记住的是,状态管理更多的是关于弄清楚状态的类型。这与我们使用的工具关系不大。