Managing State

Intermediate

title: Quản lý trạng thái

Khi ứng dụng của bạn phát triển, việc có kế hoạch hơn về cách tổ chức trạng thái và cách dữ liệu luân chuyển giữa các component sẽ rất hữu ích. Trạng thái dư thừa hoặc trùng lặp là một nguồn gốc phổ biến của lỗi. Trong chương này, bạn sẽ học cách cấu trúc trạng thái của mình một cách tốt, cách giữ cho logic cập nhật trạng thái của bạn dễ bảo trì và cách chia sẻ trạng thái giữa các component ở xa.

Phản hồi đầu vào bằng trạng thái

Với React, bạn sẽ không sửa đổi UI trực tiếp từ code. Ví dụ: bạn sẽ không viết các lệnh như “vô hiệu hóa nút”, “kích hoạt nút”, “hiển thị thông báo thành công”, v.v. Thay vào đó, bạn sẽ mô tả UI bạn muốn thấy cho các trạng thái trực quan khác nhau của component của bạn (“trạng thái ban đầu”, “trạng thái gõ”, “trạng thái thành công”), và sau đó kích hoạt các thay đổi trạng thái để đáp ứng đầu vào của người dùng. Điều này tương tự như cách các nhà thiết kế nghĩ về UI.

Đây là một biểu mẫu trắc nghiệm được xây dựng bằng React. Lưu ý cách nó sử dụng biến trạng thái status để xác định xem có nên bật hoặc tắt nút gửi hay không, và có nên hiển thị thông báo thành công hay không.

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);
  });
}

Ready to learn this topic?

Đọc Phản hồi đầu vào bằng trạng thái để tìm hiểu cách tiếp cận các tương tác với tư duy hướng đến trạng thái.

Đọc thêm

Chọn cấu trúc trạng thái

Cấu trúc trạng thái tốt có thể tạo ra sự khác biệt giữa một component dễ sửa đổi và gỡ lỗi, và một component là nguồn gốc liên tục của lỗi. Nguyên tắc quan trọng nhất là trạng thái không nên chứa thông tin dư thừa hoặc trùng lặp. Nếu có trạng thái không cần thiết, bạn rất dễ quên cập nhật nó và gây ra lỗi!

Ví dụ: biểu mẫu này có một biến trạng thái fullName dư thừa:

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>
    </>
  );
}

Bạn có thể xóa nó và đơn giản hóa code bằng cách tính toán fullName trong khi component đang render:

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>
    </>
  );
}

Điều này có vẻ như là một thay đổi nhỏ, nhưng nhiều lỗi trong các ứng dụng React được sửa theo cách này.

Ready to learn this topic?

Đọc Chọn cấu trúc trạng thái để tìm hiểu cách thiết kế hình dạng trạng thái để tránh lỗi.

Đọc thêm

Chia sẻ trạng thái giữa các component

Đôi khi, bạn muốn trạng thái của hai component luôn thay đổi cùng nhau. Để làm điều đó, hãy xóa trạng thái khỏi cả hai, di chuyển nó đến parent chung gần nhất của chúng, và sau đó truyền nó xuống cho chúng thông qua props. Điều này được gọi là “nâng trạng thái lên”, và đó là một trong những điều phổ biến nhất bạn sẽ làm khi viết code React.

Trong ví dụ này, chỉ một panel được kích hoạt tại một thời điểm. Để đạt được điều này, thay vì giữ trạng thái hoạt động bên trong mỗi panel riêng lẻ, component parent giữ trạng thái và chỉ định các props cho các component con của nó.

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>
  );
}

Ready to learn this topic?

Đọc Chia sẻ trạng thái giữa các component để tìm hiểu cách nâng trạng thái lên và giữ cho các component đồng bộ.

Đọc thêm

Giữ lại và đặt lại trạng thái

Khi bạn render lại một component, React cần quyết định phần nào của cây sẽ giữ lại (và cập nhật), và phần nào sẽ loại bỏ hoặc tạo lại từ đầu. Trong hầu hết các trường hợp, hành vi tự động của React hoạt động đủ tốt. Theo mặc định, React giữ lại các phần của cây “khớp” với cây component đã render trước đó.

Tuy nhiên, đôi khi đây không phải là điều bạn muốn. Trong ứng dụng chat này, việc nhập tin nhắn và sau đó chuyển đổi người nhận không đặt lại đầu vào. Điều này có thể khiến người dùng vô tình gửi tin nhắn cho nhầm người:

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 cho phép bạn ghi đè hành vi mặc định và buộc một component đặt lại trạng thái của nó bằng cách truyền cho nó một key khác, như <Chat key={email} />. Điều này cho React biết rằng nếu người nhận khác, nó nên được coi là một component Chat khác cần được tạo lại từ đầu với dữ liệu mới (và UI như đầu vào). Bây giờ, việc chuyển đổi giữa những người nhận sẽ đặt lại trường đầu vào—ngay cả khi bạn render cùng một component.

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' }
];

Ready to learn this topic?

Đọc Giữ lại và đặt lại trạng thái để tìm hiểu vòng đời của trạng thái và cách kiểm soát nó.

Đọc thêm

Trích xuất logic trạng thái vào một reducer

Các component có nhiều cập nhật trạng thái trải rộng trên nhiều trình xử lý sự kiện có thể trở nên quá tải. Đối với những trường hợp này, bạn có thể hợp nhất tất cả logic cập nhật trạng thái bên ngoài component của bạn trong một hàm duy nhất, được gọi là “reducer”. Các trình xử lý sự kiện của bạn trở nên ngắn gọn vì chúng chỉ định các “hành động” của người dùng. Ở cuối file, hàm reducer chỉ định cách trạng thái sẽ cập nhật để đáp ứng với mỗi hành động!

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 }
];

Ready to learn this topic?

Đọc Trích xuất logic trạng thái vào một Reducer để tìm hiểu cách hợp nhất logic trong hàm reducer.

Đọc thêm

Truyền dữ liệu sâu với context

Thông thường, bạn sẽ truyền thông tin từ một component parent đến một component con thông qua props. Nhưng việc truyền props có thể trở nên bất tiện nếu bạn cần truyền một số prop qua nhiều component, hoặc nếu nhiều component cần cùng một thông tin. Context cho phép component parent cung cấp một số thông tin cho bất kỳ component nào trong cây bên dưới nó—bất kể nó sâu đến đâu—mà không cần truyền nó một cách rõ ràng thông qua props.

Ở đây, component Heading xác định cấp độ tiêu đề của nó bằng cách “hỏi” Section gần nhất về cấp độ của nó. Mỗi Section theo dõi cấp độ của riêng mình bằng cách hỏi Section parent và thêm một vào nó. Mọi Section cung cấp thông tin cho tất cả các component bên dưới nó mà không cần truyền props—nó thực hiện điều đó thông qua 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>
  );
}

Ready to learn this topic?

Đọc Truyền dữ liệu sâu với Context để tìm hiểu về việc sử dụng context như một giải pháp thay thế cho việc truyền props.

Đọc thêm

Mở rộng với Reducer và Context

Reducers cho phép bạn hợp nhất logic cập nhật trạng thái của một component. Context cho phép bạn truyền thông tin sâu xuống các component khác. Bạn có thể kết hợp reducers và context với nhau để quản lý trạng thái của một màn hình phức tạp.

Với phương pháp này, một component parent có trạng thái phức tạp quản lý nó bằng một reducer. Các component khác ở bất kỳ đâu sâu trong cây có thể đọc trạng thái của nó thông qua context. Chúng cũng có thể dispatch các hành động để cập nhật trạng thái đó.

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>
  );
}

Ready to learn this topic?

Đọc Mở rộng với Reducer và Context để tìm hiểu cách quản lý trạng thái mở rộng trong một ứng dụng đang phát triển.

Đọc thêm

Tiếp theo là gì?

Đi tới Phản hồi đầu vào bằng trạng thái để bắt đầu đọc trang chương này từng trang một!

Hoặc, nếu bạn đã quen thuộc với các chủ đề này, tại sao không đọc về Các lối thoát hiểm?