Sửa cảnh báo 'Received true for a non-boolean attribute' khi truyền Boolean Props vào DOM Elements trong React

beginner⚛️ React2026-05-13| React 16+, React 18, Next.js, Create React App — bất kỳ dự án nào spread props vào native DOM elements (div, button, input, v.v.)

Error Message

Warning: Received `true` for a non-boolean attribute `loading`. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `loading` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
#dom#props#boolean-attribute#html#custom-props

Cảnh Báo

Bạn mở DevTools và thấy thông báo này trong console:

Warning: Received `true` for a non-boolean attribute `loading`.
If you intentionally want it to appear in the DOM as a custom attribute,
spell it as lowercase `loading` instead. If you accidentally passed it
from a parent component, remove it from the DOM element.

Một prop boolean tùy chỉnh — loading, isActive, hasError, fetching — đã bị rò rỉ xuống một phần tử HTML thực sự. React đang báo cho bạn biết rằng DOM đã nhận được thứ gì đó mà nó không hiểu.

Nguyên Nhân

Thuộc tính HTML và React props là hai thứ khác nhau. HTML chỉ nhận một tập hợp cố định các thuộc tính boolean: disabled, checked, readonly, required, và một số thuộc tính khác. Truyền loading={true} vào một DOM node thực, React không biết phải làm gì với nó — vì vậy nó cảnh báo bạn và ghi loading="true" vào HTML thực tế, điều này là không hợp lệ.

Thủ phạm thường gặp là việc spread props. Thoạt nhìn đoạn code này có vẻ ổn:

// Button.tsx
function Button({ loading, children, ...rest }) {
  return (
    // loading đã được destructure ra — nên ...rest an toàn ở đây
    <button {...rest}>
      {loading ? 'Loading...' : children}
    </button>
  );
}

Đúng vậy, thực ra đoạn đó là chính xác — loading được tách ra trước khi tạo rest. Vấn đề xảy ra khi bạn bỏ qua việc destructure hoàn toàn:

// SAI — spread tất cả mọi thứ, kể cả `loading`, lên <div>
function Card(props) {
  return <div {...props} />;
}

Hoặc một component cha truyền loading qua nhiều cấp, và ở đâu đó gần cuối chuỗi nó chạm vào một phần tử DOM mà không được lọc bỏ. Điều này đặc biệt phổ biến trong các thư viện component nơi các wrapper chuyển tiếp tất cả props một cách mù quáng.

Cách Khắc Phục Từng Bước

Cách 1: Destructure trước khi spread

Tách prop tùy chỉnh ra một cách tường minh. Nó được sử dụng bên trong component và không bao giờ đến được ...rest:

// ĐÚNG
function Button({ loading, children, ...rest }) {
  return (
    <button {...rest} disabled={loading}>
      {loading ? 'Saving...' : children}
    </button>
  );
}

Đơn giản và không tốn thêm chi phí nào. Cách này xử lý được phần lớn các trường hợp.

Cách 2: Forwarding refs

Quy tắc tương tự khi bạn sử dụng forwardRef — hãy destructure các prop tùy chỉnh trước khi spread:

const Input = React.forwardRef(function Input(
  { loading, isValid, ...rest },
  ref
) {
  return <input ref={ref} {...rest} />;
});

Cả loading lẫn isValid đều biến mất. Chỉ các thuộc tính HTML input thực sự mới chạy qua ...rest.

Cách 3: Transient props trong styled-components

Thêm tiền tố $ vào prop của bạn — transient props, có sẵn từ styled-components v5.1. Chúng được sử dụng trong quá trình tạo style và không bao giờ được chuyển tiếp xuống DOM:

// styled-components v5.1+
const StyledButton = styled.button<{ $loading?: boolean }>`
  opacity: ${(p) => (p.$loading ? 0.6 : 1)};
`;

// Cách dùng
<StyledButton $loading={isLoading}>Submit</StyledButton>

Đổi tên prop tại nơi gọi, và styled-components sẽ lo phần còn lại.

Cách 4: shouldForwardProp

Khi không thể đổi tên — một prop của bên thứ ba, API của design system mà bạn không thể thay đổi — hãy chặn nó bằng shouldForwardProp:

import styled from 'styled-components';

const StyledDiv = styled('div').withConfig({
  shouldForwardProp: (prop) => prop !== 'loading' && prop !== 'fetching',
})`
  /* styles */
`;

Emotion hoạt động theo cách tương tự:

import styled from '@emotion/styled';

const StyledDiv = styled('div', {
  shouldForwardProp: (prop) => prop !== 'loading',
})`
  /* styles */
`;

Cách 5: Lọc tường minh cho các wrapper chung

Đang xây dựng một wrapper chung chuyển tiếp các prop không xác định? Hãy duy trì một danh sách chặn và loại bỏ các key tùy chỉnh trước khi spread:

const CUSTOM_PROPS = new Set(['loading', 'isActive', 'hasError']);

function GenericWrapper({ children, ...props }: React.HTMLAttributes<HTMLDivElement> & CustomProps) {
  const domProps = Object.fromEntries(
    Object.entries(props).filter(([key]) => !CUSTOM_PROPS.has(key))
  );

  return <div {...domProps}>{children}</div>;
}

Kiểm Tra

Mở DevTools và xác nhận cảnh báo đã biến mất. Sau đó nhấp chuột phải vào phần tử → Inspect — bạn không nên thấy loading="true" hay loading="" xuất hiện trên HTML node. Việc sửa thành công nếu thuộc tính đó đã biến mất khỏi DOM và component vẫn hoạt động đúng.

Đang chạy React 18 với Strict Mode? Các component render hai lần trong môi trường development. Hãy đảm bảo cảnh báo không xuất hiện ở cả hai lần render, không chỉ lần đầu tiên.

Tham Khảo Nhanh: Những Gì React Chuyển Tiếp Mà Không Phàn Nàn

Đây là các thuộc tính boolean HTML mà React nhận ra và chuyển tiếp một cách bình thường: disabled, checked, defaultChecked, readOnly, multiple, autoFocus, required, selected. Bất kỳ thứ gì tùy chỉnh — loading, isOpen, fetching — phải được loại bỏ trước khi chạm vào một phần tử DOM.

Khi Bạn Thực Sự Muốn Nó Xuất Hiện Trong DOM

Đôi khi bạn muốn một thuộc tính tùy chỉnh hiển thị trong HTML — cho CSS selector như [data-loading] hoặc làm hook cho việc kiểm thử. Hãy dùng tiền tố data-*:

<button data-loading={isLoading ? 'true' : undefined}>
  Submit
</button>

HTML hợp lệ, không có cảnh báo React. Lưu ý một điều: thuộc tính HTML luôn là chuỗi, vì vậy hãy truyền 'true' hoặc undefined — không phải boolean.

Related Error Notes