Những sai lầm thường gặp của người mới học React

Django Developer
React là một trong những thư viện JavaScript phổ biến nhất hiện nay, nhưng những người mới bắt đầu với React thường gặp phải một số vấn đề trong quá trình học. Trong bài viết này, chúng ta sẽ tìm hiểu về những sai lầm thường gặp của người mới học React và cách khắc phục chúng.
nhung-sai-lam-thuong-gap-cua-nguoi-moi-hoc-react

Một vài năm trước, tôi đã giảng dạy React tại một trường bootcamp lập trình địa phương và tôi nhận thấy rằng có một số điều mà các sinh viên thường bị bất ngờ. Mọi người tiếp tục rơi vào những điểm bị lỗi giống nhau!

Trong hướng dẫn này, chúng ta sẽ khám phá 9 lỗi thường gặp nhất. Bạn sẽ học cách tránh chúng và hy vọng tránh được nhiều sự thất vọng.

Để giữ cho bài đăng trên blog này nhẹ nhàng và dễ chịu, chúng ta sẽ không đào sâu quá nhiều vào nguyên nhân của những lỗi này. Đây là một tài liệu tham khảo nhanh chóng hơn.

Đối tượng đọc dự kiến
Bài viết này được viết cho những nhà phát triển đã quen thuộc với cơ bản của React, nhưng vẫn khá mới trong hành trình của họ.

Evaluating with zero (So sánh với giá trị 0)

Được rồi, hãy bắt đầu với một trong những điểm bị lỗi lan truyền nhất. Tôi đã thấy cái này "ở ngoài đời" trên một số ứng dụng sản xuất!

Hãy xem xét cài đặt sau đây:

Mục tiêu của chúng ta là hiển thị danh sách mua sắm theo điều kiện. Nếu chúng ta có ít nhất 1 mục trong mảng, chúng ta nên hiển thị một phần tử ShoppingList. Nếu không có gì trong mảng, chúng ta không nên hiển thị gì cả.

Tuy nhiên, chúng ta lại nhận được một số 0 ngẫu nhiên trên giao diện người dùng!

Điều này xảy ra vì items.length đánh giá thành 0. Và vì 0 là giá trị falsy trong JavaScript, toán tử && sẽ ngắt và giá trị của biểu thức sẽ trả về 0.

Thực tế, đó giống như chúng ta đã làm điều này:

function App() {
  return (
    <div>
      {0}
    </div>
  );
}

Khác với các giá trị falsy khác ('', null, false, vv), số 0 là một giá trị hợp lệ trong JSX. Sau tất cả, có rất nhiều tình huống mà chúng ta thực sự muốn in số 0!

Cách sửa: Biểu thức của chúng ta nên sử dụng một giá trị boolean "pure" (true / false):

function App() {
  const [items, setItems] = React.useState([]);
  return (
    <div>
      {items.length > 0 && (
        <ShoppingList items={items} />
      )}
    </div>
  );
}

Cả hai tùy chọn đều hoàn toàn hợp lệ và phụ thuộc vào sở thích cá nhân.

Mutating state (Sự thay đổi của state)

Hãy tiếp tục làm việc với ví dụ danh sách mua sắm của chúng ta. Giả sử chúng ta có khả năng thêm các mục mới:

Hàm handleAddItem được gọi khi người dùng gửi một mục mới. Thật không may, nó không hoạt động! Khi chúng ta nhập một mục và gửi biểu mẫu, mục đó không được thêm vào danh sách mua sắm.

Đây là vấn đề: chúng ta đang vi phạm có lẽ là luật tối cao nhất trong React. Chúng ta đang thay đổi trạng thái.

Cụ thể, vấn đề nằm ở dòng này:

function handleAddItem(value) {
  items.push(value);
  setItems(items);
}

React dựa trên danh tính của biến trạng thái để xác định khi nào trạng thái đã thay đổi. Khi chúng ta đẩy một mục vào một mảng, chúng ta không thay đổi danh tính của mảng đó, do đó React không thể nhận ra rằng giá trị đã thay đổi.

Cách sửa: Chúng ta cần tạo một mảng hoàn toàn mới. Đây là cách tôi sẽ làm:

function handleAddItem(value) {
  const nextItems = [...items, value];
  setItems(nextItems);
}

Thay vì sửa đổi một mảng hiện có, tôi tạo ra một mảng mới từ đầu. Nó bao gồm tất cả các mục giống như trước đó (nhờ cú pháp ... spread), cũng như mục mới được nhập.

Sự khác biệt ở đây là giữa chỉnh sửa một mục hiện có và tạo một mục mới. Khi chúng ta truyền một giá trị cho một hàm thiết lập trạng thái như setCount, nó cần phải là một thực thể mới.

Điều tương tự cũng đúng đối với đối tượng:

// ❌ Mutates an existing object
function handleChangeEmail(nextEmail) {
  user.email = nextEmail;
  setUser(user);
}
// ✅ Creates a new object
function handleChangeEmail(email) {
  const nextUser = { ...user, email: nextEmail };
  setUser(nextUser);
}

Điều quan trọng là cú pháp ... là một cách để sao chép toàn bộ các phần tử từ một mảng hoặc đối tượng sang một thực thể mới. Điều này đảm bảo rằng mọi thứ hoạt động đúng cách.

Không tạo ra các key

Đây là một cảnh báo mà bạn có thể đã thấy trước đó:

Warning: Each child in a list should have a unique "key" prop.

Cách phổ biến nhất để điều này xảy ra là khi sử dụng hàm map trên dữ liệu. Sau đây là một ví dụ về vi phạm này:

Khi chúng ta hiển thị một mảng các phần tử, chúng ta cần cung cấp một chút ngữ cảnh bổ sung cho React, để nó có thể xác định mỗi mục. Quan trọng nhất, điều này cần là một định danh duy nhất (unique identifier).

Nhiều tài nguyên trực tuyến sẽ đề xuất sử dụng chỉ số của mảng để giải quyết vấn đề này:

function ShoppingList({ items }) {
  return (
    <ul>
      {items.map((item, index) => {
        return (
          <li key={index}>{item}</li>
        );
      })}
    </ul>
  );
}

Tôi không nghĩ đây là một lời khuyên tốt. Cách tiếp cận này sẽ hoạt động trong một số trường hợp, nhưng nó có thể gây ra một số vấn đề khá lớn trong các trường hợp khác.

Khi bạn có được sự hiểu biết sâu sắc hơn về cách React hoạt động, bạn có thể xác định liệu điều đó có tốt hay không cho từng trường hợp, nhưng thực sự, tôi nghĩ rằng nó dễ giải quyết vấn đề một cách an toàn trong mọi trường hợp. Như vậy, bạn không bao giờ phải lo lắng về nó!

Đây là kế hoạch: Mỗi khi một mục mới được thêm vào danh sách, chúng ta sẽ tạo ra một ID duy nhất cho nó:

function handleAddItem(value) {
  const nextItem = {
    id: crypto.randomUUID(),
    label: value,
  };
  const nextItems = [...items, nextItem];
  setItems(nextItems);
}

`crypto.randomUUID là một phương thức được tích hợp sẵn trong trình duyệt (không phải là gói phần mềm bên thứ ba). Nó có sẵn trên tất cả các trình duyệt chính. Nó không liên quan gì đến tiền điện tử.

Phương thức này tạo ra một chuỗi duy nhất, như d9bb3c4c-0459-48b9-a94c-7ca3963f7bd0.

Bằng cách tạo ra một ID mới mỗi khi người dùng gửi mẫu, chúng ta đảm bảo rằng mỗi mục trong danh sách mua hàng có một ID duy nhất.

Dưới đây là cách chúng ta áp dụng ID như một key:

function ShoppingList({ items }) {
  return (
    <ul>
      {items.map((item, index) => {
        return (
          <li key={item.id}>
            {item.label}
          </li>
        );
      })}
    </ul>
  );
}

Quan trọng là chúng ta muốn tạo ID khi state được cập nhật. Chúng ta không muốn làm như thế này:

// ❌ This is a bad idea
<li key={crypto.randomUUID()}>
  {item.label}
</li>

Việc tạo key trong JSX như thế này sẽ làm cho key thay đổi trong mỗi lần render. Khi key thay đổi, React sẽ phá hủy và tạo lại các phần tử này, điều này có thể ảnh hưởng tiêu cực đến hiệu suất.

Mẫu này -- tạo key khi dữ liệu được tạo lần đầu tiên -- có thể được áp dụng cho nhiều tình huống khác nhau. Ví dụ, đây là cách tôi tạo ID duy nhất khi lấy dữ liệu từ máy chủ:

const [data, setData] = React.useState(null);
async function retrieveData() {
  const res = await fetch('/api/data');
  const json = await res.json();
  // The moment we have the data, we generate
  // an ID for each item:
  const dataWithId = json.data.map(item => {
    return {
      ...item,
      id: crypto.randomUUID(),
    };
  });
  // Then we update the state with
  // this augmented data:
  setData(dataWithId);
}

Thiếu khoảng trắng

Đây là một lỗi thường gặp trên web.

Lưu ý rằng hai câu bị nén lại với nhau:

Lỗi này xảy ra vì trình biên dịch JSX (công cụ biến đổi JSX mà chúng ta viết thành mã JavaScript dễ đọc cho trình duyệt) không thể phân biệt được khoảng trắng ngữ pháp và khoảng trắng mà chúng ta thêm để canh giữ mã cho dễ đọc.

Cách khắc phục: chúng ta cần thêm một ký tự khoảng trắng rõ ràng giữa văn bản và thẻ mở.

<p>
  Chào mừng đến với Vietdev.com!
  {' '}
  <a href="/login">Đăng nhập để tiếp tục</a>
</p>

Một lời khuyên nhỏ: nếu bạn sử dụng Prettier, nó sẽ tự động thêm những ký tự khoảng trắng này cho bạn! Chỉ cần để cho Prettier làm định dạng mã (không tách các dòng mã trước).

Tại sao đội ngũ React không giải quyết vấn đề này??

Khi tôi lần đầu tiên biết đến chiến lược này, tôi cảm thấy nó rất lộn xộn. Tại sao đội ngũ React không sửa chữa để nó hoạt động theo cách chúng ta mong đợi?!

Sau đó, tôi nhận ra rằng không có giải pháp hoàn hảo cho vấn đề này. Nếu React bắt đầu hiểu rằng thụt đầu dòng là khoảng trắng ngữ pháp, nó sẽ giải quyết vấn đề này, nhưng lại gây ra rất nhiều vấn đề khác.

Cuối cùng, dù cảm thấy hacky, tôi nghĩ đó là quyết định đúng đắn. Đó là phương án tốt nhất!

Truy cập trạng thái sau khi thay đổi nó

Điều này bắt ai cũng bất ngờ ở một thời điểm nào đó. Khi tôi dạy tại một trại huấn luyện mã nguồn mở địa phương, tôi đã mất điểm số của số lần mọi người đến tìm tôi với vấn đề này.

Đây là một ứng dụng đếm tối thiểu: bấm vào nút tăng giá trị đếm. Hãy xem xem bạn có thể nhận ra vấn đề:

Sau khi tăng biến trạng thái count, chúng ta đang ghi log giá trị của nó ra console. Thật đáng ngạc nhiên, nó đang ghi log giá trị sai.

Đây là vấn đề: các hàm setter của state trong React như setCount là bất đồng bộ.

Đây là đoạn code gây vấn đề:

function handleClick() {
  setCount(count + 1);
  console.log({ count });
}

Dễ dàng nhầm tưởng rằng setCount hoạt động giống như việc gán giá trị, như khi làm như thế này:

count = count + 1;
console.log({ count });

Điều này không phải là cách React được xây dựng. Khi chúng ta gọi setCount, chúng ta không gán lại một biến. Chúng ta đang lên lịch cập nhật.

Có thể mất một thời gian để hiểu rõ ý tưởng này, nhưng đây là điều gì đó có thể giúp nó dễ hiểu hơn: chúng ta không thể gán lại biến đếm, vì nó là một hằng số!

// Uses `const`, not `let`, and so it can't be reassigned
const [count, setCount] = React.useState(0);
count = count + 1; // Uncaught TypeError:
                   // Assignment to constant variable

Vậy làm sao để khắc phục vấn đề này? May mắn thay, chúng ta đã biết được giá trị của biến count cần được sử dụng. Chúng ta cần lưu giữ giá trị này trong một biến để có thể truy cập vào nó sau đó:

function handleClick() {
  const nextCount = count + 1;
  setCount(nextCount);
  // Use `nextCount` whenever we want
  // to reference the new value:
  console.log({ nextCount });
}

Tôi thích sử dụng tiền tố "next" khi làm việc như thế này (nextCount, nextItems, nextEmail, vv). Điều này làm cho tôi hiểu rõ hơn rằng chúng ta không cập nhật giá trị hiện tại, mà chúng ta đang lên lịch cho giá trị tiếp theo.

return về nhiều component cùng lúc

Thỉnh thoảng chúng ta cũng cần trả về đa thành phần. Ví dụ như sau:

Chúng ta muốn LabeledInput component của chúng ta trả về hai phần tử: một thẻ <label> và một thẻ <input>. Tuy nhiên, chúng ta gặp lỗi:

Adjacent JSX elements must be wrapped in an enclosing tag. (Liền kề nhau các phần tử JSX phải được bao bọc trong một thẻ.)

Lỗi này xảy ra vì JSX được biên dịch thành JavaScript thông thường. Đây là cách mã này trông như sau khi đưa vào trình duyệt:

function LabeledInput({ id, label, ...delegated }) {
  return (
    React.createElement('label', { htmlFor: id }, label)
    React.createElement('input', { id: id, ...delegated })
  );
}

Trong JavaScript, chúng ta không được phép trả về nhiều giá trị như thế này. Đó cũng chính là lý do tại sao điều này không hoạt động:

function addTwoNumbers(a, b) {
  return (
    "the answer is"
    a + b
  );
}

Để khắc phục lỗi này, trong một thời gian dài, cách tiếp cận tiêu chuẩn là bọc cả hai phần tử trong một thẻ bao bọc, ví dụ như một thẻ div:

function LabeledInput({ id, label, ...delegated }) {
  return (
    <div>
      <label htmlFor={id}>
        {label}
      </label>
      <input
        id={id}
        {...delegated}
      />
    </div>
  );
}

Bằng cách gom <label><input> của chúng ta trong một <div>, chúng ta chỉ trả về một phần tử cấp độ trên duy nhất!

Đây là ví dụ về plain JS tương ứng với mã trên:

function LabeledInput({ id, label, ...delegated }) {
  return React.createElement(
    'div',
    {},
    React.createElement('label', { htmlFor: id }, label),
    React.createElement('input', { id: id, ...delegated })
  );
}

JSX là một sự trừu tượng tuyệt vời, nhưng thường làm mờ đi những sự thật cơ bản về JavaScript. Tôi nghĩ rằng thường rất hữu ích để xem cách JSX được chuyển đổi thành JS thuần túy, để hiểu được những gì đang xảy ra.

Với cách tiếp cận mới này, chúng ta đang trả về một phần tử duy nhất và phần tử đó chứa hai phần tử con. Vấn đề đã được giải quyết!

Chúng ta có thể cải thiện giải pháp này hơn nữa bằng cách sử dụng các đoạn mã fragment.

function LabeledInput({ id, label, ...delegated }) {
  return (
    <React.Fragment>
      <label htmlFor={id}>
        {label}
      </label>
      <input
        id={id}
        {...delegated}
      />
    </React.Fragment>
  );
}

React.Fragment là một thành phần của React được tạo ra để giải quyết vấn đề này. Nó cho phép chúng ta đóng gói nhiều phần tử cấp cao nhất mà không ảnh hưởng đến DOM. Điều này rất tuyệt vời: nó có nghĩa là chúng ta không làm ô nhiễm đánh dấu với một <div> không cần thiết.

Nó cũng có một cú pháp thuận tiện. Chúng ta có thể viết fragments như sau:

function LabeledInput({ id, label, ...delegated }) {
  return (
    <>
      <label htmlFor={id}>
        {label}
      </label>
      <input
        id={id}
        {...delegated}
      />
    </>
  );
}

Tôi thích biểu tượng ở đây: nhóm React đã chọn sử dụng một thẻ HTML rỗng, <>, để cho thấy các fragment không tạo ra bất kỳ markup thực tế nào.

Sử dụng thuộc tính style bị sai (thiếu dấu ngoặc nhọn)

Trong JSX, cú pháp và cách viết rất giống HTML, nhưng có một số khác biệt bất ngờ giữa hai loại ngôn ngữ này thường khiến mọi người bị lẫn.

Hầu hết các khác biệt này đã được tài liệu hóa rõ ràng và các cảnh báo trên console thường rất mô tả và hữu ích. Ví dụ, nếu bạn vô tình sử dụng class thay vì className, React sẽ cho bạn biết chính xác vấn đề là gì.

Nhưng có một khác biệt tinh subtile mà thường làm người ta gặp rắc rối: thuộc tính style.

Trong HTML, style được viết dưới dạng một chuỗi:

<button style="color: red; font-size: 1.25rem">
  Hello World
</button>

Ở trong JSX, chúng ta cần chỉ định thuộc tính style dưới dạng một đối tượng với các tên thuộc tính được viết theo kiểu camelCase.

Dưới đây, tôi đã cố gắng làm đúng như vậy, nhưng lại gặp lỗi. Bạn có thể nhận ra lỗi gì không?

Vấn đề là tôi cần sử dụng hai dấu ngoặc nhọn kép, như thế này:

<button
  {/* "{{", instead of "{": */}
  style={{ color: 'red', fontSize: '1.25rem' }}
>
  Hello World
</button>

Để hiểu tại sao điều này cần thiết, chúng ta cần khai quật vào cú pháp này một chút.

Trong JSX, chúng ta sử dụng dấu ngoặc nhọn để tạo một khe biểu thức. Chúng ta có thể đặt bất kỳ biểu thức JS hợp lệ nào vào khe này. Ví dụ:

<button className={isPrimary ? 'btn primary' : 'btn'}>

Bất kỳ thứ gì chúng ta đặt giữa {} sẽ được đánh giá là JavaScript, và kết quả sẽ được đặt cho thuộc tính này. className sẽ là 'btn primary' hoặc 'btn'.

Với style, trước tiên chúng ta cần tạo một expression slot, và sau đó chúng ta muốn truyền một object JavaScript vào slot này.

Tôi nghĩ rõ hơn nếu chúng ta rút ra đối tượng thành một biến:

{/* 1. Create the style object: */}
const btnStyles = { color: 'red', fontSize: '1.25rem' };
{/* 2. Pass that object to the `style` attribute: */}
<button style={btnStyles}>
  Hello World
</button>
{/* Or, we can do it all in 1 step: */}
<button style={{ color: 'red', fontSize: '1.25rem' }}>

Đoạn mã ngoặc nhọn bên ngoài tạo ra một "khe biểu thức" trong JSX. Đoạn mã ngoặc nhọn bên trong tạo ra một đối tượng JavaScript chứa các kiểu dáng của chúng ta.

Hàm tác động không đồng bộ

Hãy giả sử chúng ta có một hàm lấy dữ liệu người dùng từ API của chúng ta khi nó được mount. Chúng ta sẽ sử dụng hook useEffect và chúng ta muốn sử dụng từ khóa await.

Đây là phiên bản đầu tiên của tôi:

Đáng tiếc, chúng ta nhận được một lỗi:

'await' is only allowed within async functions

Okay, không có vấn đề gì. Hãy cập nhật callback của effect thành một hàm async, bằng cách thêm tiền tố async vào đầu:

React.useEffect(async () => {
  const url = `${API}/get-profile?id=${userId}`;
  const res = await fetch(url);
  const json = await res.json();
  setUser(json);
}, [userId]);

Đáng tiếc là, điều này không hoạt động; chúng ta nhận được một thông báo lỗi khó hiểu:

destroy is not a function

Đây là cách để sửa lỗi: Chúng ta cần tạo một hàm riêng biệt là async trong effect của chúng ta:

React.useEffect(() => {
  // Create an async function...
  async function runEffect() {
    const url = `${API}/get-profile?id=${userId}`;
    const res = await fetch(url);
    const json = await res.json();
    setUser(json);
  }
  // ...and then invoke it:
  runEffect();
}, [userId]);

Để hiểu tại sao giải pháp tạm thời này lại cần thiết, thì đáng để suy nghĩ xem từ khóa async thực sự làm gì.

Ví dụ, bạn nghĩ hàm này trả về gì?

async function greeting() {
  return "Hello world!";
}

Đầu tiên, có vẻ rõ ràng rằng nó trả về chuỗi "Hello world!"! Nhưng thực sự, hàm này trả về một promise. Promise này giải quyết thành chuỗi "Hello world!".

Điều này là một vấn đề, vì useEffect hook không mong đợi chúng ta trả về một promise! Nó mong đợi chúng ta trả về hoặc không có gì cả (như chúng ta đã làm ở trên), hoặc một hàm dọn dẹp.

Hàm dọn dẹp là quá nhiều ngoài phạm vi của bài hướng dẫn này, nhưng chúng rất quan trọng. Hầu hết các hiệu ứng của chúng tôi sẽ có một loại logic phá hủy, và chúng tôi cần cung cấp nó cho React ASAP, để React có thể gọi nó khi các phụ thuộc thay đổi hoặc thành phần bị unmount.

Với chiến lược "hàm async riêng biệt" của chúng tôi, chúng tôi vẫn có thể trả về một hàm dọn dẹp ngay lập tức:

React.useEffect(() => {
  async function runEffect() {
    // Effect logic here
  }
  runEffect();
  return () => {
    // Cleanup logic here
  }
}, [userId]);

Bạn có thể đặt tên cho hàm này bất cứ thứ gì bạn muốn, nhưng tôi thích tên chung là runEffect. Nó làm rõ rằng nó chứa logic chính của hiệu ứng.

Phát triển trực giác

Khi nhìn lướt qua, nhiều cách sửa lỗi chúng ta đã thấy trong bài hướng dẫn này có vẻ khá tùy ý. Tại sao chúng ta cần cung cấp một khóa key duy nhất? Tại sao chúng ta không thể truy cập trạng thái sau khi thay đổi nó? Và tại sao useEffect lại quá khó chịu?!

React luôn luôn khá khó để thực sự thoải mái khi làm việc với nó, đặc biệt là với hooks. Cần một thời gian để tất cả mọi thứ trở nên dễ hiểu.

Tôi bắt đầu sử dụng React từ năm 2015 và tôi nhớ tôi đã nghĩ: "Điều này thật tuyệt vời, nhưng tôi không có ý tưởng về cách nó hoạt động." 😅

Kể từ đó, tôi đã xây dựng mô hình tư duy của mình về React một mảnh ghép một lần. Tôi đã có một loạt những bất ngờ, và mỗi lần, mô hình tư duy của tôi trở nên vững chắc và mạnh mẽ hơn. Tôi bắt đầu hiểu tại sao React hoạt động như vậy.

Tôi thấy tôi không cần phải nhớ các quy tắc tùy ý; thay vào đó, tôi có thể tin vào trực giác của mình. Khó có thể diễn tả được sự vui vẻ mà React mang lại cho tôi!

Trong năm qua, tôi đã phát triển một khóa học trực tuyến tương tác có tên The Joy of React. Đó là một khóa học dành cho người mới bắt đầu với mục tiêu giúp bạn xây dựng trực giác của mình về cách React hoạt động, để bạn có thể sử dụng nó để xây dựng các ứng dụng web động phong phú.

Dịch từ bài gốc của tác giả Joshua Comeau: https://www.joshwcomeau.com/react/common-beginner-mistakes/

Công nghệ được nhắc đến trong bài viết này

Tên Công NghệPhiên BảnPhát Hành
React18-Liên kết
22 phút đọc·308 lượt xem·