LoginSignup
1
0

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

Last updated at Posted at 2024-03-30

なんでそんなことをする?

種々のスタイル群を選んで使用できるコンポネントを作成し、
さらにそのコンポーネントに対して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);
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0