概要
styled-componentsは拡張元のコンポーネントにtheme
という名前のpropを渡さないようです。
そのため、拡張元のコンポーネントにtheme
という名前のpropを渡したい場合は少し工夫をする必要があります。
以下の説明で使用するサンプルコードは以下のURLに載せてあります。
https://github.com/ishioka0222/styled-components-theme-prop
経緯
Ant DesignというReact用のUIフレームワークに、theme
という名前のpropの値に応じて見た目を変更できるコンポーネントがあるのですが、そのコンポーネントをstyled-componentsで拡張したところ、theme
という名前のpropの値だけが反映されなくなりました。
調べたところ、styled-componentsの実装に原因があることが分かったのですが、バグというよりは想定された挙動のようにも見えました。
備忘録として、ここに事象とその対処法について書き残しておきます。
動作を確認した環境
- node.js: v18.18.0
- react: v18.2.0
- react-dom: v18.2.0
- styled-components: v6.1.0
事象について
例えば、外部のライブラリに、theme
というpropの値に応じてfilled
とoutlined
の2つの見た目を選べるようなThirdPartyComponent
というコンポーネントがあるとします。
type ThirdPartyTheme = "filled" | "outlined";
interface ThirdPartyComponentProps {
className?: string;
theme: ThirdPartyTheme;
}
const ThirdPartyComponent: React.FC<
PropsWithChildren<ThirdPartyComponentProps>
> = (props) => {
return (
<div
className={props.className}
style={{
padding: "0.5rem",
margin: "0.5rem 0",
border: "1px solid dodgerblue",
backgroundColor:
props.theme === "filled" ? "dodgerblue" : "transparent",
color: props.theme === "filled" ? "whitesmoke" : "dodgerblue",
}}
>
{`theme: ${props.theme}`}
<hr />
{props.children}
</div>
);
};
function App() {
return (
<>
<ThirdPartyComponent theme={"filled"}>
ThirdPartyComponent's children...
</ThirdPartyComponent>
<ThirdPartyComponent theme={"outlined"}>
ThirdPartyComponent's children...
</ThirdPartyComponent>
</>
);
}
このコンポーネントのtheme
の値にfilled
、outlined
を指定すると、それぞれ以下のような見た目になります。
次に、このコンポーネントに影を付けようと思い、以下のようにstyled-componentsで拡張したとします。
const StyledThirdPartyComponent = styled(ThirdPartyComponent)`
box-shadow: 0 8px 8px 0px #333;
`;
function App() {
return (
<>
<StyledThirdPartyComponent theme={"filled"}>
StyledThirdPartyComponent's children...
</StyledThirdPartyComponent>
<StyledThirdPartyComponent theme={"outlined"}>
StyledThirdPartyComponent's children...
</StyledThirdPartyComponent>
</>
);
}
すると、ThirdPartyComponent
が受け取ったtheme
の値はundefined
になってしまいます。
こうなってしまう理由はよく分からないのですが、styled-componentsのソースコードを見ると、以下の部分でtheme
というpropを意図的に読み飛ばしているようです。
if (context[key] === undefined) {
// Omit undefined values from props passed to wrapped element.
// This enables using .attrs() to remove props, for example.
} else if (key[0] === '$' || key === 'as' || key === 'theme') {
// Omit transient props and execution props.
} else if (key === 'forwardedAs') {
propsForElement.as = context.forwardedAs;
styled-componentsにもtheme機能があるため、それとの兼ね合いでしょうか・・・。
対処法について
styled-componentsのGitHub Issuesでもこの件は報告されていますが、theme
というpropの名前を変更して回避するしかないようです。
具体的にはtheme
と被らないようなprop名を適当に用意します。ここでは仮に_theme
とします1。
そして、styled-componentsでスタイルを拡張するときに、_theme
で受け取った値を、theme
という名前で拡張元のコンポーネントに渡すようにします。
type StyledThirdPartyComponentProps = Omit<
ThirdPartyComponentProps,
"theme"
> & { _theme: ThirdPartyTheme };
const StyledThirdPartyComponent = styled(
({ _theme, ...props }: PropsWithChildren<StyledThirdPartyComponentProps>) => (
<ThirdPartyComponent {...props} theme={_theme} />
)
)`
box-shadow: 0 8px 8px 0px #333;
`;
function App() {
return (
<>
<StyledThirdPartyComponent _theme={"filled"}>
StyledThirdPartyComponent's children...
</StyledThirdPartyComponent>
<StyledThirdPartyComponent _theme={"outlined"}>
StyledThirdPartyComponent's children...
</StyledThirdPartyComponent>
</>
);
}
こうすれば、以下の画像のように_theme
の値が適用され、影も付けることができます。
上の説明で使用したサンプルコードは以下のURLに載せてあります。
https://github.com/ishioka0222/styled-components-theme-prop
-
ここの変数名は基本的にはなんでも構いませんが、
$
から始まるpropはstyled-componentsでtransient propと呼ばれ、拡張元のコンポーネントに渡されないため、一文字目には$
以外の文字を使用する必要があります。 ↩