React + StyleXを使用したカスタム可能な共通の関数コンポーネントをつくる

# なんでそんなことをする？

種々のスタイル群を選んで使用できるコンポネントを作成し、
さらにそのコンポーネントに対してStyleXのスタイルを渡せるようにしたかった。



# ⭕️ 1つの回答例

共通コンポーネントにスタイルのタイプ引数によりスタイルを分岐させる。

- ユニオン型でスタイルのタイプを定義する
- スタイルのキーと引数を完全に対応させるためにRecordを使用する
- 複数のスタイル指定を許容できるように引数を配列にして、reduceでスタイルをガッチャンコ



## 共通コンポーネント

```tsx:/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);

```



## 使用例

### スクロール動作のテストページ



```tsx:/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だからということで型のみを共通化させた場合も試したが、
それも分化コンポーネントの記述内容の大部分を共通することになってしまった。

そのため、コンポーネントをスタイルごとに分化させる試みは諦めることになった。


### 共通コンポーネント

```tsx: /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);
```

### 分化コンポーネント


```tsx: /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);
```