警告の内容
DevToolsを開くと、コンソールに次のような警告が表示されます:
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.
カスタムのboolean型プロップ — loading、isActive、hasError、fetching — が実際のHTML要素にまで漏れ出しています。ReactはDOMが理解できないものを受け取ったと警告しているのです。
なぜこれが起きるのか
HTML属性とReactのプロップは別物です。HTMLが認識するboolean属性は固定されており、disabled、checked、readonly、requiredなど、ごく一部に限られています。実際のDOMノードにloading={true}を渡すと、Reactはどう処理すればよいかわからず、警告を出した上で実際のHTMLにloading="true"と書き込んでしまいます。これは無効な記述です。
よくある原因はプロップのスプレッドです。一見すると問題なさそうに見えます:
// Button.tsx
function Button({ loading, children, ...rest }) {
return (
// loading is destructured out — so ...rest is safe here
<button {...rest}>
{loading ? 'Loading...' : children}
</button>
);
}
これは実際には正しい書き方です — loadingはrestに含まれる前に取り出されています。問題が起きるのは、デストラクチャリングを省略してしまった場合です:
// BAD — spreads everything, including `loading`, onto <div>
function Card(props) {
return <div {...props} />;
}
あるいは、親コンポーネントがloadingを複数階層にわたって渡し続け、どこかで取り除かれないままDOMに届いてしまうケースもあります。これは特に、ラッパーがすべてのプロップをそのまま転送するコンポーネントライブラリでよく見られます。
修正手順
修正1:スプレッドの前にデストラクチャリングする
カスタムプロップを明示的に取り出します。コンポーネント内で消費されるため、...restに含まれることはありません:
// GOOD
function Button({ loading, children, ...rest }) {
return (
<button {...rest} disabled={loading}>
{loading ? 'Saving...' : children}
</button>
);
}
シンプルでオーバーヘッドもゼロです。大半のケースはこれで解決します。
修正2:refの転送
forwardRefを使う場合も同じルールです — スプレッドの前にカスタムプロップをデストラクチャリングしてください:
const Input = React.forwardRef(function Input(
{ loading, isValid, ...rest },
ref
) {
return <input ref={ref} {...rest} />;
});
loadingとisValidはどちらも除外されます。...restには本来のHTML input属性だけが流れます。
修正3:styled-componentsのtransient props
プロップ名の先頭に$を付けます — styled-components v5.1から使えるtransient propsです。スタイリング時に消費され、DOMには転送されません:
// styled-components v5.1+
const StyledButton = styled.button<{ $loading?: boolean }>`
opacity: ${(p) => (p.$loading ? 0.6 : 1)};
`;
// Usage
<StyledButton $loading={isLoading}>Submit</StyledButton>
呼び出し側でプロップ名を変更するだけで、styled-componentsが残りの処理を行います。
修正4:shouldForwardProp
名前を変更できない場合 — サードパーティのプロップや変更できないデザインシステムのAPIなど — shouldForwardPropでブロックします:
import styled from 'styled-components';
const StyledDiv = styled('div').withConfig({
shouldForwardProp: (prop) => prop !== 'loading' && prop !== 'fetching',
})`
/* styles */
`;
Emotionでも同じように使えます:
import styled from '@emotion/styled';
const StyledDiv = styled('div', {
shouldForwardProp: (prop) => prop !== 'loading',
})`
/* styles */
`;
修正5:汎用ラッパーでの明示的なフィルタリング
未知のプロップを転送する汎用ラッパーを作成する場合は、ブロックリストを管理し、スプレッドの前にカスタムキーを取り除きます:
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>;
}
確認方法
DevToolsを開いて警告が消えていることを確認します。次に、対象の要素を右クリックして「検証」を選択し、HTMLノードにloading="true"やloading=""が残っていないことを確認してください。属性がDOMから消えており、コンポーネントが正常に動作していれば修正は成功です。
Strict ModeのReact 18を使用している場合、コンポーネントは開発環境で2回レンダリングされます。最初のレンダリングだけでなく、両方のレンダリングで警告が出ないことを確認してください。
クイックリファレンス:Reactが問題なく転送するHTML属性
ReactがHTMLのboolean属性として認識し、問題なく転送するものは次のとおりです:disabled、checked、defaultChecked、readOnly、multiple、autoFocus、required、selected。loading、isOpen、fetchingなどのカスタムプロップは、DOM要素に渡る前に必ず取り除く必要があります。
DOMに表示させたい場合
CSSセレクタ([data-loading]など)やテスト用のフックとして、カスタム属性をHTMLに表示させたいこともあります。その場合はdata-*プレフィックスを使用してください:
<button data-loading={isLoading ? 'true' : undefined}>
Submit
</button>
有効なHTMLであり、Reactの警告も発生しません。ひとつ注意点があります:HTML属性は常に文字列なので、booleanではなく'true'またはundefinedを渡してください。

