Skip to content

Latest commit

 

History

History
249 lines (199 loc) · 11.8 KB

2021.07.md

File metadata and controls

249 lines (199 loc) · 11.8 KB

【译】优化 React 应用中的状态管理

原文地址: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>
  );
}

一个有两个文本输入的表单。用户在两个字段中都输入数值。我们默认禁用该按钮,只有在有两个值的情况下才启用。请注意,我们需要 skilllsyears 在一起,这样我们才能计算出 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>
  );
}

这里我们有一个页眉和一个页脚。它们都需要知道应用程序的当前主题。这个例子很简单,但想象一下,ThemeThemeSelector 是深深嵌套在其他组件中的组件。它们不能直接访问 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);
  });
}

saveDataloadData 是用来模拟数据库的。我们将 localStorage 的 API 包装在 promises 中,它是同步的,以使它看起来需要一些时间。我们将假装查询一个远程数据库。<Example> 组件使用 useQueryuseMutation 来加载和保存数据。有一种技术可以实现乐观的更新,这将使我们的演示变得优雅简洁。它将看起来像我们用 useState 做的。

当然,在这个例子中,我们只是触及到了表面。像 ReactQuery 这样的库可以做得更多。数据缓存、数据失效、错误处理、回退逻辑,等等。各种各样的事情,我们通常自己做。

总结

状态管理的优化始于确定我们有什么样的状态。然后是选择合适的工具来完成这项工作。现在,React 作为一个库提供了许多管理状态的工具。Hooks 和 context API 对于大多数情况来说已经足够好了。如果它们不适合,那么我们有各种第三方工具供我们使用。例如,ReduxRedux toolkit,仍然能够使用。像 ReactQuery 这样的库步入了试图在生态系统中找到自己的位置的阶段。在工具方面,我们有很多不同的选择。最后我们应该记住的是,状态管理更多的是关于弄清楚状态的类型。这与我们使用的工具关系不大。