管理状态

中级

随着应用程序的增长,更注重状态的组织方式以及数据在组件之间的流动方式将大有裨益。冗余或重复的状态是错误的常见来源。在本章中,您将学习如何良好地构建状态、如何保持状态更新逻辑的可维护性以及如何在相距较远的组件之间共享状态。

使用状态响应输入

使用 React,您不会直接从代码修改 UI。例如,您不会编写诸如“禁用按钮”、“启用按钮”、“显示成功消息”之类的命令。相反,您将描述希望为组件的不同视觉状态(“初始状态”、“键入状态”、“成功状态”)看到的 UI,然后响应用户输入触发状态更改。这类似于设计师对 UI 的思考方式。

这是一个使用 React 构建的测验表单。请注意,它如何使用status状态变量来确定是启用还是禁用提交按钮,以及是否显示成功消息。

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>That's right!</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>City quiz</h2>
      <p>
        In which city is there a billboard that turns air into drinkable water?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Pretend it's hitting the network.
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Good guess but a wrong answer. Try again!'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

准备好学习这个主题了吗?

阅读 使用状态响应输入,学习如何以状态驱动的心态来处理交互。

阅读更多

选择状态结构

良好地构建状态可以在易于修改和调试的组件与不断出现错误的组件之间产生差异。最重要的原则是状态不应包含冗余或重复的信息。如果存在不必要的状态,则很容易忘记更新它并引入错误!

例如,此表单具有 冗余fullName状态变量

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

您可以通过在组件渲染时计算fullName来移除它并简化代码

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

这看起来可能是一个很小的更改,但许多 React 应用程序中的错误都是这样修复的。

准备好学习这个主题了吗?

阅读 选择状态结构,学习如何设计状态形状以避免错误。

阅读更多

在组件之间共享状态

有时,您希望两个组件的状态始终一起更改。为此,请从这两个组件中移除状态,将其移动到它们最近的公共父级,然后通过 props 将其传递给它们。这被称为“提升状态”,这是编写 React 代码时最常见的事情之一。

在此示例中,一次只能激活一个面板。为此,不是将活动状态保存在每个单独的面板中,而是父组件持有状态并为其子级指定 props。

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Show
        </button>
      )}
    </section>
  );
}

准备好学习这个主题了吗?

阅读 在组件之间共享状态,学习如何提升状态并保持组件同步。

阅读更多

保留和重置状态

重新渲染组件时,React 需要决定树的哪些部分需要保留(并更新),哪些部分需要丢弃或从头重新创建。在大多数情况下,React 的自动行为已经足够好用。默认情况下,React 会保留与先前渲染的组件树“匹配”的树的部分。

但是,有时这并不是你想要的结果。在这个聊天应用中,键入消息然后切换收件人不会重置输入框。这可能会导致用户意外地向错误的人发送消息。

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: '[email protected]' },
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' }
];

React 允许你覆盖默认行为,并通过传递一个不同的 key (例如 <Chat key={email} />) 来强制组件重置其状态。这告诉 React,如果收件人不同,则应将其视为需要从头重新创建的新 Chat 组件,并使用新的数据(和UI,例如输入框)。现在,在收件人之间切换会重置输入字段——即使你渲染的是同一个组件。

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Taylor', email: '[email protected]' },
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' }
];

准备好学习这个主题了吗?

阅读 状态的保留和重置 以了解状态的生命周期以及如何控制它。

阅读更多

将状态逻辑提取到 reducer 中

组件中许多状态更新分散在许多事件处理程序中,可能会让人难以应付。对于这些情况,你可以在组件外部的单个函数(称为“reducer”)中合并所有状态更新逻辑。你的事件处理程序变得简洁,因为它们只指定用户“操作”。在文件的底部,reducer 函数指定状态如何响应每个操作进行更新!

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Prague itinerary</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

准备好学习这个主题了吗?

阅读 将状态逻辑提取到 reducer 中 以了解如何在 reducer 函数中合并逻辑。

阅读更多

使用 context 深层传递数据

通常,你会通过 props 将信息从父组件传递到子组件。但是,如果你需要通过许多组件传递某些 props,或者许多组件需要相同的信息,那么传递 props 可能会变得不方便。Context 允许父组件将其中的某些信息提供给其下方树中的任何组件——无论它有多深——而无需通过 props 明确传递它。

在这里,Heading 组件通过“询问”最近的 Section 组件的级别来确定其标题级别。每个 Section 组件通过询问父 Section 组件并加 1 来跟踪它自己的级别。每个 Section 组件都向其下方的所有组件提供信息,而无需传递 props——它通过 context 来做到这一点。

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Title</Heading>
      <Section>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Heading>Heading</Heading>
        <Section>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Heading>Sub-heading</Heading>
          <Section>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
            <Heading>Sub-sub-heading</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

准备好学习这个主题了吗?

阅读 使用 Context 深层传递数据 以了解如何使用 context 作为传递 props 的替代方法。

阅读更多

使用 reducer 和 context 扩展应用

Reducers 允许你合并组件的状态更新逻辑。Context 允许你将信息深层传递到其他组件。你可以将 reducers 和 context 组合在一起以管理复杂屏幕的状态。

使用这种方法,具有复杂状态的父组件使用 reducer 来管理它。树中任何深层的其他组件都可以通过 context 读取其状态。它们还可以分派操作来更新该状态。

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

准备好学习这个主题了吗?

阅读 使用 reducer 和 context 扩展应用 以了解状态管理如何在不断发展的应用程序中扩展。

阅读更多

下一步是什么?

前往 使用状态响应输入 开始逐页阅读本章!

或者,如果你已经熟悉这些主题,为什么不阅读关于 应急方案 的内容呢?