美文网首页
第二七章 管理状态-序

第二七章 管理状态-序

作者: 深圳都这么冷 | 来源:发表于2023-02-15 15:46 被阅读0次

    管理状态

    随着您的应用程序的增长,更有意识地了解您的状态是如何组织的以及数据如何在您的组件之间流动是有帮助的。 冗余或重复状态是错误的常见来源。 在本章中,您将学习如何很好地构建状态,如何保持状态更新逻辑的可维护性,以及如何在远距离组件之间共享状态。

    本章内容预告

    • 如何考虑 UI 随着状态的变化而变化
    • 如何很好地构建状态
    • 如何“提升状态”以在组件之间共享它
    • 如何控制状态是被保留还是重置
    • 如何在一个函数中整合复杂的状态逻辑
    • 如何在没有“prop钻探”的情况下传递信息
    • 如何随着应用的增长扩展状态管理

    对状态输入做出反应

    使用 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 保留树中与先前渲染的组件树“匹配”的部分。

    但是,有时这不是您想要的。 例如,在此应用中,键入消息然后切换收件人不会重置输入。 这可能会使用户不小心将消息发送给错误的人:
    Chat.js

    import { useState } from 'react';
    
    export default function Chat({ contact }) {
      const [text, setText] = useState('');
      return (
        <section className="chat">
          <textarea
            value={text}
            placeholder={'Chat to ' + contact.name}
            onChange={e => setText(e.target.value)}
          />
          <br />
          <button>Send to {contact.email}</button>
        </section>
      );
    }
    

    ContactList.js

    import { useState } from 'react';
    
    export default function Chat({ contact }) {
      const [text, setText] = useState('');
      return (
        <section className="chat">
          <textarea
            value={text}
            placeholder={'Chat to ' + contact.name}
            onChange={e => setText(e.target.value)}
          />
          <br />
          <button>Send to {contact.email}</button>
        </section>
      );
    }
    

    App.js

    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: 'taylor@mail.com' },
      { name: 'Alice', email: 'alice@mail.com' },
      { name: 'Bob', email: 'bob@mail.com' }
    ];
    

    React 允许您覆盖默认行为,并通过向组件传递不同的键来强制组件重置其状态,例如 <Chat key={email} />。 这告诉 React,如果接收者不同,它应该被视为一个不同的聊天组件,需要使用新数据(和 UI 之类的输入)从头开始重新创建。 现在在收件人之间切换总是会重置输入字段——即使您呈现相同的组件。
    Chat.js

    import { useState } from 'react';
    
    export default function Chat({ contact }) {
      const [text, setText] = useState('');
      return (
        <section className="chat">
          <textarea
            value={text}
            placeholder={'Chat to ' + contact.name}
            onChange={e => setText(e.target.value)}
          />
          <br />
          <button>Send to {contact.email}</button>
        </section>
      );
    }
    
    

    ContactList.js

    export default function ContactList({
      selectedContact,
      contacts,
      onSelect
    }) {
      return (
        <section className="contact-list">
          <ul>
            {contacts.map(contact =>
              <li key={contact.email}>
                <button onClick={() => {
                  onSelect(contact);
                }}>
                  {contact.name}
                </button>
              </li>
            )}
          </ul>
        </section>
      );
    }
    
    

    App.js

    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: 'taylor@mail.com' },
      { name: 'Alice', email: 'alice@mail.com' },
      { name: 'Bob', email: 'bob@mail.com' }
    ];
    

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

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

    将状态逻辑提取到 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 将信息从父组件传递到子组件。 但是如果你需要通过许多组件传递一些 prop,或者如果许多组件需要相同的信息,那么传递 props 会变得不方便。 Context 允许父组件向其下方树中的任何组件提供一些信息——无论它有多深——而无需通过 props 显式传递。

    在这里,Heading组件通过“询问”最接近的Section来确定其标题级别。 每个Section通过询问父Section并向其添加一个来跟踪自己的级别。 每个 Section 都在不传递 props 的情况下向它下面的所有组件提供信息——它通过上下文来做到这一点。
    LevelContext.js

    import { createContext } from 'react';
    
    export const LevelContext = createContext(0);
    

    Heading.js

    import { useContext } from 'react';
    import { LevelContext } from './LevelContext.js';
    
    export default function Heading({ children }) {
      const level = useContext(LevelContext);
      switch (level) {
        case 0:
          throw Error('Heading must be inside a Section!');
        case 1:
          return <h1>{children}</h1>;
        case 2:
          return <h2>{children}</h2>;
        case 3:
          return <h3>{children}</h3>;
        case 4:
          return <h4>{children}</h4>;
        case 5:
          return <h5>{children}</h5>;
        case 6:
          return <h6>{children}</h6>;
        default:
          throw Error('Unknown level: ' + level);
      }
    }
    

    Section.js

    import { useContext } from 'react';
    import { LevelContext } from './LevelContext.js';
    
    export default function Section({ children }) {
      const level = useContext(LevelContext);
      return (
        <section className="section">
          <LevelContext.Provider value={level + 1}>
            {children}
          </LevelContext.Provider>
        </section>
      );
    }
    

    App.js

    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)深度传递数据了解使用上下文作为传递 props 的替代方法。

    使用 reducer 和 context 进行扩展

    Reducers 让您可以整合组件的状态更新逻辑。 上下文使您可以将信息深入传递给其他组件。 您可以将 reducers 和上下文结合在一起来管理复杂屏幕的状态。

    通过这种方法,具有复杂状态的父组件使用 reducer 对其进行管理。 树中任何深处的其他组件都可以通过上下文读取其状态。 他们还可以调度操作来更新该状态。
    TaskList.js

    import { useState, useContext } from 'react';
    import { useTasks, useTasksDispatch } from './TasksContext.js';
    
    export default function TaskList() {
      const tasks = useTasks();
      return (
        <ul>
          {tasks.map(task => (
            <li key={task.id}>
              <Task task={task} />
            </li>
          ))}
        </ul>
      );
    }
    
    function Task({ task }) {
      const [isEditing, setIsEditing] = useState(false);
      const dispatch = useTasksDispatch();
      let taskContent;
      if (isEditing) {
        taskContent = (
          <>
            <input
              value={task.text}
              onChange={e => {
                dispatch({
                  type: 'changed',
                  task: {
                    ...task,
                    text: e.target.value
                  }
                });
              }} />
            <button onClick={() => setIsEditing(false)}>
              Save
            </button>
          </>
        );
      } else {
        taskContent = (
          <>
            {task.text}
            <button onClick={() => setIsEditing(true)}>
              Edit
            </button>
          </>
        );
      }
      return (
        <label>
          <input
            type="checkbox"
            checked={task.done}
            onChange={e => {
              dispatch({
                type: 'changed',
                task: {
                  ...task,
                  done: e.target.checked
                }
              });
            }}
          />
          {taskContent}
          <button onClick={() => {
            dispatch({
              type: 'deleted',
              id: task.id
            });
          }}>
            Delete
          </button>
        </label>
      );
    }
    

    AddTask.js

    import { useState, useContext } from 'react';
    import { useTasksDispatch } from './TasksContext.js';
    
    export default function AddTask({ onAddTask }) {
      const [text, setText] = useState('');
      const dispatch = useTasksDispatch();
      return (
        <>
          <input
            placeholder="Add task"
            value={text}
            onChange={e => setText(e.target.value)}
          />
          <button onClick={() => {
            setText('');
            dispatch({
              type: 'added',
              id: nextId++,
              text: text,
            });
          }}>Add</button>
        </>
      );
    }
    
    let nextId = 3;
    

    TaskContext.js

    import { createContext, useContext, useReducer } from 'react';
    
    const TasksContext = createContext(null);
    const TasksDispatchContext = createContext(null);
    
    export function TasksProvider({ children }) {
      const [tasks, dispatch] = useReducer(
        tasksReducer,
        initialTasks
      );
    
      return (
        <TasksContext.Provider value={tasks}>
          <TasksDispatchContext.Provider
            value={dispatch}
          >
            {children}
          </TasksDispatchContext.Provider>
        </TasksContext.Provider>
      );
    }
    
    export function useTasks() {
      return useContext(TasksContext);
    }
    
    export function useTasksDispatch() {
      return useContext(TasksDispatchContext);
    }
    
    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);
        }
      }
    }
    
    const initialTasks = [
      { id: 0, text: 'Philosopher’s Path', done: true },
      { id: 1, text: 'Visit the temple', done: false },
      { id: 2, text: 'Drink matcha', done: false }
    ];
    

    App.js

    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 进行扩展了解状态管理如何在不断增长的应用程序中扩展。

    下一步是什么?

    前往对状态输入做出反应开始逐页阅读本章!
    或者,如果您已经熟悉这些主题,为什么不阅读逃生窗口的内容?

    相关文章

      网友评论

          本文标题:第二七章 管理状态-序

          本文链接:https://www.haomeiwen.com/subject/jfgxkdtx.html