Lựa chọn cấu trúc cho state

Cấu trúc state tốt có thể tạo ra sự khác biệt giữa một component dễ chỉnh sửa và debug và một component bị lỗi liên tục. Sau đây là một số mẹo bạn nên cân nhắc khi cấu trúc state.

Bạn sẽ được học

  • Khi nào nên sử dụng nhiều state hay một state duy nhất cho nhiều giá trị
  • Những điều cần tránh khi tổ chức state
  • Cách để fix những lỗi phổ biến khi cấu trúc state

Nguyên tắc khi cấu trúc state

Khi bạn viết một component có chứa một vài state, bạn sẽ phải đưa ra quyết định về việc có bao nhiêu state cần sử dụng và cấu trúccủa chúng. Mặc dù có thể viết chương trình đúng ngay cả khi cấu trúc state không tối ưu, nhưng có một vài nguyên tắc có thể giúp bạn đưa ra những lựa chọn tốt hơn:

  1. Nhóm các state có liên quan. Nếu bạn luôn phải cập nhật hai hoặc nhiều hơn state cùng một lúc, hãy nghĩ đến việc gộp chúng vào một state duy nhất.
  2. Tránh sự mâu thuẫn trong state. Khi state được cấu trúc sao cho một số phần của state có thể mâu thuẫn và “không đồng ý” với nhau, bạn để lại cơ hội cho lỗi. Hãy cố gắng tránh điều này.
  3. Tránh dư thừa state. Nếu bạn có thể tính toán một số thông tin từ props của component hoặc các state hiện tại của nó trong quá trình render, bạn không nên đặt thông tin đó vào state của component đó.
  4. Tránh trùng lặp trong state. Khi cùng một data được lặp lại giữa nhiều state hoặc trong các object lồng nhau, rất khó để giữ cho chúng đồng bộ với nhau. Hạn chế sự trùng lặp này khi bạn có thể.
  5. Tránh lồng state quá sâu. State có cấu trúc phân cấp sâu rất không thuận tiện để cập nhật. Khi có thể, hãy ưu tiên cấu trúc state theo cách phẳng.

Mục tiêu đằng sau các quy tắc này là làm cho state dễ dàng cập nhật mà không gây ra lỗi. Xoá data dư thừa và trùng lặp khỏi state giúp đảm bảo rằng tất cả các phần của nó luông đồng bộ. Điều này gần giống với cách một database engineer muốn “chuẩn hoá” cấu trúc database để giảm khả năng xảy ra lỗi. Để dùng lời của Albert Einstein, “Hãy làm cho state của bạn đơn giản nhất có thể—nhưng không đơn giản hơn.”

Giờ hãy xem cách các nguyên tắc này được áp dụng trong thực tế.

Đôi khi bạn có thể không chắc chắn giữa việc sử dụng nhiều state hay một state duy nhất cho nhiều giá trị.

Bạn nên làm như thế này?

const [x, setX] = useState(0);
const [y, setY] = useState(0);

Hay như thế này?

const [position, setPosition] = useState({ x: 0, y: 0 });

Về mặt kỹ thuật, bạn có thể sử dụng một trong hai cách trên. Nhưng nếu hai state luôn thay đổi cùng nhau, việc gộp chúng lại với nhau có thể là một ý tưởng tốt. Khi đó, bạn không cần phải lo lắng về việc giữ cho chúng đồng bộ, giống như trong ví dụ dưới đây khi di chuyển con trỏ sẽ cập nhật cả hai tọa độ của chấm đỏ:

import { useState } from 'react';

export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          y: e.clientY
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  )
}

Một trường hợp khác là bạn sẽ nhóm data vào một object hoặc một mảng khi bạn không biết bạn sẽ cần bao nhiêu state. Ví dụ, nó rất hữu ích khi bạn có một form mà người dùng có thể thêm các trường tùy chỉnh.

Chú Ý

Nếu state của bạn là một object, hãy nhớ rằng bạn không thể chỉ cập nhật một trường của nó mà không phải sao chép các trường khác. Ví dụ, bạn không thể gọi setPosition({ x: 100 }) trong ví dụ trên vì nó sẽ không có trường y nào cả! Thay vào đó, nếu bạn muốn chỉ cập nhật x, bạn sẽ phải gọi setPosition({ ...position, x: 100 }), hoặc chia chúng thành hai state và gọi setX(100).

Tránh mâu thuẫn trong state

Đây là một form phản hồi của khách sạn với state isSendingisSent:

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Cảm ơn bạn đã phản hồi!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p></p>
      <p>Bạn thấy kỳ nghỉ của mình tại The Prancing Pony thế nào?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Gửi
      </button>
      {isSending && <p>Đang gửi...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Trong khi đoạn code này hoạt động, nó để lại cơ hội cho các trạng thái “không thể xảy ra” xảy ra. Ví dụ, nếu bạn quên gọi setIsSentsetIsSending cùng một lúc, bạn có thể kết thúc trong tình huống mà cả isSendingisSent đều là true cùng một lúc. Component của bạn càng phức tạp, việc hiểu xem đã xảy ra điều gì càng khó khăn.

isSendingisSent không nên cùng true trong bất kỳ trường hợp nào, tốt hơn là nó nên được thay thế bởi một state được gọi là status và nó có thể mang một trong các giá trị sau: 'typing' (ban đầu), 'sending', và 'sent':

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Cảm ơn bạn đã phản hồi!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>Bạn thấy kỳ nghỉ của mình tại The Prancing Pony như thế nào?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Gửi
      </button>
      {isSending && <p>Đang gửi...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

Bạn cũng có thể khai báo thêm một số hằng số để dễ đọc:

const isSending = status === 'sending';
const isSent = status === 'sent';

Vì chúng không phải là state, nên bạn không cần phải lo lắng về việc chúng không đồng bộ với nhau.

Tránh dư thừa state

Nếu bạn có thể tính toán một số thông tin từ props của component hoặc các state hiện tại của nó trong quá trình render, bạn không nên đặt thông tin đó vào state của component đó.

Ví dụ, hãy xem form này. Nó hoạt động, nhưng bạn có thể tìm thấy bất kỳ state nào dư thừa không?

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(lastName + ' ' + e.target.value);
  }

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

  return (
    <>
      <h2>Đăng ký thông tin</h2>
      <label>
        Họ:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Tên:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <p>
        Vé của bạn sẽ được cấp cho: <b>{fullName}</b>
      </p>
    </>
  );
}

Form này có chứa ba state: firstName, lastNamefullName. Tuy nhiên, fullName là dư thừa. Bạn luôn có thể tính được fullName từ firstNamelastName khi render, do đó hãy xoá nó khỏi state.

Đây là cách bạn có thể làm điều đó:

import { useState } from 'react';

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

  const fullName = lastName + ' ' + firstName;

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

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

  return (
    <>
      <h2>Đăng ký thông tin</h2>
      <label>
        Họ:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Tên:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <p>
        Vé của bạn sẽ được cấp cho: <b>{fullName}</b>
      </p>
    </>
  );
}

Giờ đây, fullName không là state. Thay vào đó, nó được tính toán trong quá trình render:

const fullName = lastName + ' ' + firstName;

As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName or setLastName, you trigger a re-render, and then the next fullName will be calculated from the fresh data.

Do đó, các handler không cần phải làm bất cứ điều gì đặc biệt để cập nhật nó. Khi bạn gọi setFirstName hoặc setLastName, bạn kích hoạt một lần re-render, và sau đó fullName sẽ được tính toán từ dữ liệu mới.

Tìm hiểu sâu

Đừng sao chép props vào state

Một ví dụ cho sự dư thừa state phổ biến là đoạn code như sau:

function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

Ở đây, color mang giá trị khỏi tạo là prop messageColor. Vấn đề là nếu component cha truyền một giá trị khác của messageColor sau này (ví dụ, 'red' thay vì 'blue'), biến color state variable sẽ không được cập nhật! State chỉ được khởi tạo trong lần render đầu tiên.

Đây là lý do tại sao sao chép một số prop vào một state có thể dẫn đến sự nhầm lẫn. Thay vào đó, hãy sử dụng prop messageColor trực tiếp trong code của bạn. Nếu bạn muốn đặt tên ngắn gọn hơn, hãy gán cho nó một hằng số:

function Message({ messageColor }) {
const color = messageColor;

Bằng cách này, nó sẽ đồng bộ với prop được truyền từ component cha.

Sao chép props vào state chỉ hợp lý khi bạn muốn bỏ qua tất cả các cập nhật cho một prop cụ thể. Theo quy ước, bắt đầu tên prop với initial hoặc default để làm rõ rằng các giá trị mới của nó bị bỏ qua:

function Message({ initialColor }) {
// State `color` mang giá trị *đầu tiên* của `initialColor`.
// Các thay đổi sau này của prop `initialColor` sẽ bị bỏ qua.
const [color, setColor] = useState(initialColor);

Tránh trùng lặp trong state

Component Menu này cho phép bạn chọn một món ăn từ danh sách và hiển thị món ăn đã chọn:

import { useState } from 'react';

const initialItems = [
  { title: 'phở', id: 0 },
  { title: 'bún chả', id: 1 },
  { title: 'bánh mì', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>Bạn muốn dùng món gì</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Chọn</button>
          </li>
        ))}
      </ul>
      <p>Bạn đã chọn {selectedItem.title}.</p>
    </>
  );
}

Hiện tại, nó lưu món ăn được chọn dưới dạng một object trong state selectedItem. Tuy nhiên, điều này là không tốt: nội dung của selectedItem giống một object trong danh sách items. Điều này có nghĩa là thông tin về món ăn đó được lặp lại ở hai nơi.

Tại sao điều này là một vấn đề? Hãy thử cho phép người dùng chỉnh sửa món ăn trong danh sách:

import { useState } from 'react';

const initialItems = [
  { title: 'phở', id: 0 },
  { title: 'bún chả', id: 1 },
  { title: 'bánh mì', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>Bạn muốn dùng món gì?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Chọn</button>
          </li>
        ))}
      </ul>
      <p>Bạn đã chọn {selectedItem.title}.</p>
    </>
  );
}

Hãy để ý là khi bạn nhấn “Chọn” một món ăn sau đó chỉnh sửa món đó, ô input được cập nhật nhưng nhãn ở dưới không phản ánh những chỉnh sửa. Điều này xảy ra vì bạn đã trùng lặp state, và bạn đã quên cập nhật selectedItem.

Mặc dù bạn cũng có thể cập nhật selectedItem, một cách fix dễ hơn là xoá bỏ sự trùng lặp. Trong ví dụ này, thay vì một object selectedItem (tạo ra sự trùng lặp với các object trong items), bạn giữ selectedId trong state, và sau đó lấy selectedItem bằng cách tìm kiếm mảng items để tìm một item với ID đó:

import { useState } from 'react';

const initialItems = [
  { title: 'phở', id: 0 },
  { title: 'bún chả', id: 1 },
  { title: 'bánh mì', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>Bạn muốn dùng món gì?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Chọn</button>
          </li>
        ))}
      </ul>
      <p>Bạn đã chọn {selectedItem.title}.</p>
    </>
  );
}

State được sử dụng trước đây trông như thế này:

  • items = [{ id: 0, title: 'phở' }, ...]
  • selectedItem = { id: 0, title: 'phở' }

Nhưng sau khi thay đổi, nó trông như thế này:

  • items = [{ id: 0, title: 'phở'}, ...]
  • selectedId = 0

Sự trùng lặp đã biến mất, và bạn chỉ giữ lại state cần thiết!

Giờ nếu bạn chỉnh sửa món ăn đã chọn, nội dung tin nhắn bên dưới sẽ cập nhật ngay lập tức. Điều này xảy ra vì setItems kích hoạt một lần re-render, và items.find(...) sẽ tìm thấy món ăn với tiêu đề đã cập nhật. Bạn không cần giữ lại món đã chọn trong state, vì chỉ ID đã chọn mới là cần thiết. Phần còn lại có thể được tính toán trong quá trình render.

Tránh sử dụng state lồng nhau quá sâu

Hãy tưởng tượng một kế hoạch du lịch bao gồm các hành tinh, châu lục và quốc gia. Bạn có thể muốn cấu trúc state của nó bằng cách sử dụng các object và mảng lồng nhau, như trong ví dụ này:

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Trái Đất',
    childPlaces: [{
      id: 2,
      title: 'Châu Phi',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'Nam Phi',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Châu Mỹ',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Châu Á',
      childPlaces: [{
        id: 20,
        title: 'Trung Quốc',
        childPlaces: []
      }, {
        id: 21,
        title: 'Ấn Độ',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'Hàn Quốc',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thái Lan',
        childPlaces: []
      }, {
        id: 25,
        title: 'Việt Nam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Châu Âu',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'Pháp',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Đức',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Thổ Nhĩ Kỳ',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Châu Đại Dương',
      childPlaces: [{
        id: 35,
        title: 'Úc',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Mặt Trăng',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Sao Hoả',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

Giờ giả sử bạn muốn thêm một cái nút để xoá một địa điểm mà bạn đã ghé thăm. Bạn sẽ làm như thế nào? Cập nhật state lồng nhau liên quan đến việc sao chép các object từ phần đã thay đổi. Xoá một địa điểm sâu sẽ liên quan đến việc sao chép toàn bộ chuỗi cha của nó. Đoạn code như vậy có thể rất dài dòng.

Nếu state quá lồng nhau để cập nhật dễ dàng, hãy xem xét làm cho nó “phẳng”. Dưới đây là một cách bạn có thể cấu trúc lại dữ liệu này. Thay vì một cấu trúc giống cây với mỗi place có một mảng các địa điểm con của nó, bạn có thể làm cho mỗi địa điểm giữ một mảng các ID địa điểm con của nó. Sau đó map từng ID đến địa điểm tương ứng.

Cấu trúc mới này có thể khiến bạn nhớ đến việc xem một bảng cơ sở dữ liệu:

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Trái Đất',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Châu Phi',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'Nam Phi',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Châu Mỹ',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Châu Á',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'Trung Quốc',
    childIds: []
  },
  21: {
    id: 21,
    title: 'Ấn Độ',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'Hàn Quốc',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thái Lan',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Việt Nam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Châu Âu',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'Pháp',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Đức',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Thổ Nhĩ Kỳ',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Châu Đại Dương',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Úc',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Mặt Trăng',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Sao Hoả',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

Giờ khi state đã “phẳng” (còn được gọi là “chuẩn hoá”), việc cập nhật các mục lồng nhau trở nên dễ dàng hơn.

Để xoá một địa điểm bây giờ, bạn chỉ cần thực hiện hai cập nhật state:

  • Xoá ID của nó khỏi mảng childIds của địa điểm cha.
  • Cập nhật object state gốc để nó không chứa địa điểm đó nữa.

Đây là một ví dụ về cách bạn có thể thực hiện điều đó:

import { useState } from 'react';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // Tạo một phiên bản mới của địa điểm cha
    // mà không bao gồm ID con này.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Cập nhật object state gốc...
    setPlan({
      ...plan,
      // ...để nó có cha đã cập nhật.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Địa điểm tham quan</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Hoàn thành
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

Bạn có thể lồng state bao nhiêu cũng được, nhưng làm cho nó “phẳng” có thể giải quyết nhiều vấn đề. Nó giúp cập nhật state dễ dàng hơn, và đảm bảo bạn không có sự trùng lặp ở các phần khác nhau của một object lồng nhau.

Tìm hiểu sâu

Cải thiện việc sử dụng bộ nhớ

Một cách lý tưởng, bạn cũng nên xoá các mục đã xoá (và các mục con của chúng!) khỏi object “bảng” để cải thiện việc sử dụng bộ nhớ. Phiên bản này thực hiện điều đó. Nó cũng sử dụng Immer để làm cho logic cập nhật ngắn gọn hơn.

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Đôi khi, bạn cũng có thể giảm thiểu việc lồng state bằng cách di chuyển một số state lồng vào các component con. Điều này hoạt động tốt cho state UI tạm thời mà không cần lưu trữ, như việc kiểm tra một item có được hover hay không.

Tóm tắt

  • Nếu hai state luôn luôn cập nhật cùng nhau, hãy xem xét việc gộp chúng thành một.
  • Chọn cẩn thận các biến state của bạn để tránh tạo ra các trạng thái “không thể xảy ra”.
  • Cấu trúc state của bạn sao cho giảm khả năng bạn sẽ mắc lỗi khi cập nhật nó.
  • Tránh state trùng lặp và dư thừa để bạn không cần phải đồng bộ chúng.
  • Không đặt props vào state trừ khi bạn muốn ngăn cập nhật.
  • Đối với UI như chọn lựa, giữ ID hoặc index trong state thay vì chính object đó.
  • Nếu việc cập nhật state lồng nhau quá phức tạp, hãy thử làm phẳng nó.

Challenge 1 of 4:
Fix một component không cập nhật

Component Clock này nhận vào hai props: colortime. Khi bạn chọn một màu khác trong hộp chọn, component Clock nhận một color khác từ component cha của nó. Tuy nhiên, vì một lý do nào đó, màu hiển thị không cập nhật. Tại sao? Hãy fix lỗi này.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}