(2年ほど前のメモが残っていたので記事化…)
前提
- 普通にReactで書いたコンポーネントを、react-to-webcomponentでウェブコンポーネントに変換した。
- 親画面とスタイルのスコープを分離したいので、ShadowDOMにする。
環境
- React 18.1.0
- TypeScript 4.6.4
- Material UI(MUI) v5
- react-to-webcomponent
困ったこと
MUIを使用したウェブコンポーネントを親コンポーネントで表示させると、スタイルが無効化されてしまう(MUI が適用されない)
なぜこのようなことが起きるか
- 親でウェブコンポーネントを表示させた際、MUIのスタイル定義は親側のstyle要素に挿入される。
- しかし、ウェブコンポーネントはShadowDOMであるため、親→子にスタイル適用ができずにMUIのスタイル定義が無効化されてしまう。
だめだった実装例
以下のように書くと、Material UIのButtonコンポーネントを使っていても親画面からウェブコンポーネントを表示させた際にスタイルが無効化されてしまい、普通のボタンになってしまう。
const SampleComponent: React.FC<{ data: string }> = ({ data }) => {
return (
<div>
<MuiThemeProvider theme={createTheme()}>
<Box>
// Material UI のButton コンポーネント
<Button>クリック</Button>
</Box>
</MuiThemeProvider>
</div>
);
};
SampleComponent.propTypes = {
data: PropTypes.string,
};
const sampleWebComponent = reactToWebComponent(
SampleComponent,
React,
ReactDOM,
{
shadow: 'open',
}
);
customElements.define('sample-wc', sampleWebComponent);
対応
以下のようなラッパーを定義して、この中で目的のコンポーネントを使う。
Material UI v4での実装
当初はMaterial UI v4 で実装しており、後でアップグレードして後述するv5の実装にしました。
もう需要はあまり無いとは思いますが、v4では以下のような実装になります。
import { CssBaseline, ThemeProvider } from '@material-ui/core';
import { jssPreset, StylesProvider } from '@material-ui/styles';
import { create } from 'jss';
import React, { ReactNode, useState } from 'react';
// theme は MUI のcreateTheme で別途定義したもの
import theme from 'src/theme';
const WebComponentStyle: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [jss, setJss] = useState(null);
function setRefAndCreateJss(headRef) {
if (headRef && !jss) {
const createdJssWithRef = create({
...jssPreset(),
insertionPoint: headRef,
});
setJss(createdJssWithRef);
}
}
return (
<div>
<style ref={setRefAndCreateJss}></style>
{jss && (
<>
<CssBaseline />
<ThemeProvider theme={theme}>
<StylesProvider jss={jss} sheetsManager={new Map()}>
{children}
</StylesProvider>
</ThemeProvider>
</>
)}
</div>
);
};
MUI v5での実装
上記をv5向けに改修したものが以下になります。
emotionのCacheProviderを使用しています。
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { CssBaseline, ThemeProvider } from '@mui/material';
import { jssPreset, StylesProvider } from '@mui/styles';
import { create } from 'jss';
import React, { ReactNode, useState } from 'react';
// theme は MUI のcreateTheme で別途定義したもの
import theme from 'src/theme';
const WebComponentStyle: React.FC<{ children: ReactNode }> = ({
children,
}) => {
const [jss, setJss] = useState(null);
const [cache, setCache] = useState(null);
function setRefAndCreateJss(headRef) {
if (headRef && !jss) {
const createdJssWithRef = create({
...jssPreset(),
insertionPoint: headRef,
});
setJss(createdJssWithRef);
const _cache = createCache({
key: 'css',
prepend: true,
container: headRef,
});
setCache(_cache);
}
}
return (
<div>
<style ref={setRefAndCreateJss}></style>
{jss && (
<>
<ThemeProvider theme={theme}>
<CssBaseline />
<StylesProvider jss={jss} sheetsManager={new Map()}>
<CacheProvider value={cache}>
{children}
</CacheProvider>
</StylesProvider>
</ThemeProvider>
</>
)}
</div>
);
};
補足
- StylesProviderに
sheetsManager={new Map()}
を指定しないと、親でWebコンポーネントを複数並べたときにCSSが無効化される。
参考:https://stackoverflow.com/questions/62883738/material-ui-inside-shadow-dom-is-missing-styles-for-subsequent-invocation-of-web