なんでそんなことをする?
種々のスタイル群を選んで使用できるコンポネントを作成し、
さらにそのコンポーネントに対してStyleXのスタイルを渡せるようにしたかった。
⭕️ 1つの回答例
共通コンポーネントにスタイルのタイプ引数によりスタイルを分岐させる。
- ユニオン型でスタイルのタイプを定義する
- スタイルのキーと引数を完全に対応させるためにRecordを使用する
- 複数のスタイル指定を許容できるように引数を配列にして、reduceでスタイルをガッチャンコ
共通コンポーネント
/src/components/div/DivCustom.tsx
import type { StyleXStyles } from '@stylexjs/stylex';
import stylex from '@stylexjs/stylex';
import type { UserAuthoredStyles } from '@stylexjs/stylex/lib/StyleXTypes';
import { type HTMLAttributes, forwardRef, memo } from 'react';
type TypedStylesKeys =
| 'center'
| 'flexCenter'
| 'flexStart'
| 'flexColumn'
| 'gap'
| 'margin'
| 'margin2';
interface Props extends HTMLAttributes<HTMLDivElement> {
styles?: StyleXStyles<UserAuthoredStyles>;
styleTypes?: TypedStylesKeys[];
}
const typedStyles: Record<
TypedStylesKeys,
StyleXStyles<UserAuthoredStyles>
> = stylex.create({
center: {
display: 'grid',
placeItems: 'center',
textAlign: 'center',
},
flexCenter: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
},
flexStart: {
display: 'flex',
justifyContent: 'center',
alignItems: 'start',
},
flexColumn: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
},
gap: {
gap: '1em',
},
margin: {
margin: '1em',
},
margin2: {
margin: '2em',
},
});
const getTypedStyles = (
styleTypes?: TypedStylesKeys[],
): StyleXStyles<UserAuthoredStyles> | undefined =>
styleTypes?.reduce((accStyles, currentType) => {
const styleToAdd = typedStyles[currentType];
if (styleToAdd) {
// Avoid the use of spread (`...`) syntax on accumulators.
Object.assign(accStyles, styleToAdd);
}
return accStyles;
}, {});
const Component = forwardRef<HTMLDivElement, Props>(
({ styles, styleTypes, children, ...attrs }, ref) => (
<div
ref={ref}
{...attrs}
{...stylex.props(getTypedStyles(styleTypes), styles)}
>
{children}
</div>
),
);
export const DivCustom = memo(Component);
使用例
スクロール動作のテストページ
/src/pages/Scroll.tsx
import stylex from '@stylexjs/stylex';
import { type FC, memo, useCallback, useRef } from 'react';
import { scroller } from 'react-scroll';
import { ButtonVite } from '../components/button/ButtonVite';
import { DivCustom } from '../components/div/DivCustom';
const styles = stylex.create({
wrap: {
height: '200svh',
},
wraps: {
position: 'relative',
margin: '2em',
borderRadius: 32,
borderStyle: 'solid',
borderWidth: 2,
borderColor: 'lightgray',
textAlign: 'center',
height: 480,
overflow: 'auto',
'::-webkit-scrollbar': {
display: 'none',
},
},
wrapsDiv: {
height: 960,
padding: '4em',
background: 'radial-gradient(circle, #ff0000, #0000ff)',
},
target: {
position: 'absolute',
top: 0,
width: 4,
height: 4,
backgroundColor: 'red',
},
fixed: {
position: 'fixed',
right: 0,
bottom: 0,
},
});
const SCROLL_SUB = 300;
const Component: FC = () => {
const refWindowTarget = useRef<HTMLDivElement>(null);
const refDivNormal = useRef<HTMLDivElement>(null);
const refDivTarget = useRef<HTMLDivElement>(null);
const scrollWindowNormal = useCallback((top: number) => {
window.scrollTo(0, top);
}, []);
const scrollWindowSmooth = useCallback((top: number) => {
if (!refWindowTarget.current) return;
refWindowTarget.current.style.top = `${top}px`;
scroller.scrollTo('window-target', {
duration: 500,
delay: 0,
smooth: 'ease-in',
});
}, []);
const scrollDivNormal = useCallback((top: number) => {
if (!refDivNormal.current) return;
refDivNormal.current.scrollTop = top;
}, []);
const scrollDivSmooth = useCallback((top: number) => {
if (!refDivTarget.current) return;
refDivTarget.current.style.top = `${top}px`;
scroller.scrollTo('div-target', {
duration: 500,
delay: 0,
smooth: 'ease-in',
containerId: 'div-wrap',
});
}, []);
return (
<DivCustom styleTypes={['flexStart']} styles={styles.wrap}>
<div
ref={refWindowTarget}
id='window-target'
{...stylex.props(styles.target)}
/>
<DivCustom
styleTypes={['flexColumn', 'gap', 'margin']}
styles={styles.fixed}
>
<ButtonVite onClick={() => scrollWindowNormal(SCROLL_SUB)}>
Normal Scroll
</ButtonVite>
<ButtonVite onClick={() => scrollWindowSmooth(SCROLL_SUB)}>
Smooth Scroll
</ButtonVite>
</DivCustom>
{/* 通常スクロール */}
<DivCustom styles={styles.wraps} ref={refDivNormal}>
<DivCustom styles={styles.wrapsDiv}>
<ButtonVite onClick={() => scrollDivNormal(SCROLL_SUB)}>
Normal Scroll
</ButtonVite>
</DivCustom>
</DivCustom>
{/* スムーススクロール */}
<DivCustom id='div-wrap' styles={styles.wraps}>
<DivCustom styles={styles.wrapsDiv}>
<ButtonVite onClick={() => scrollDivSmooth(SCROLL_SUB)}>
Smooth Scroll
</ButtonVite>
<div
ref={refDivTarget}
id='div-target'
{...stylex.props(styles.target)}
/>
</DivCustom>
</DivCustom>
</DivCustom>
);
};
export const Scroll = memo(Component);
解説
❌️ 実装してみたが、個人的にダメだった例
スタイルのタイプごとにコンポーネントをつくる
コンポーネントごとにファイルをつくるコロケーションの観点はいいとしても、
その分化コンポーネントファイルの記述内容が
他の分化コンポーネントとほぼ同じになることが許容できなかった。
以下の例では共通コンポーネントを作成した場合のコードを紹介している。
どうせdivだからということで型のみを共通化させた場合も試したが、
それも分化コンポーネントの記述内容の大部分を共通することになってしまった。
そのため、コンポーネントをスタイルごとに分化させる試みは諦めることになった。
共通コンポーネント
/src/components/div/Common.tsx
export interface Props extends HTMLAttributes<HTMLDivElement> {
styles?: StyleXStyles<UserAuthoredStyles>;
}
const Component: FC<Props> = ({ styles, children, ...attrs }) => (
<div {...attrs} {...stylex.props(styles)}>
{children}
</div>
);
export const Common = memo(Component);
分化コンポーネント
/src/components/div/DivMargin.tsx
const styles = stylex.create({
base: {
margin: '1em',
},
});
const Component: FC<Props> = ({ styles, children, ...attrs }) => (
<Common {...attrs} {...stylex.props(styles.base)}>
{children}
</Common>
);
export const Common = memo(Component);