技術的負債に打ち勝つReactコンポーネント

技術的負債に打ち勝つReactコンポーネント

January 4, 2022

補足

先日同様のテーマで別の記事を投稿していますが、先の記事を実際に作成したのはかなり昔のことで(作成から投稿までラグがありました)、現在は本記事の書き方が私のなかでの推奨となっています。


現在、私の中でベストプラクティスになっている React コンポーネントの書き方をご紹介します。

コードを見れば意図が伝わるものと信じ、解説は省略させていただきます。

作成物 #

Component

上記は以下のファイルで構成されています。pages/index.tsx がエントリポイントとなるコンポーネントです。

├ pages/
│ ├ index.tsx
│ ├ Heading.tsx
│ ├ EvenOdd.tsx
│ ├ IncrementButton.tsx
│ └ ResetButton.tsx
└ types/
  └ index.ts

共通の型定義 #

types/index.ts

export type Empty = Record<string, never>;

export type MayStyled<T extends object> = T extends Empty
  ? { className?: string }
  : T & { className?: string };

export type Styled<T extends object> = T & { className: string };

export type UnStyled<T extends object> = Omit<T, 'className'>;

コンポーネント #

pages/index.tsx

import styled from '@emotion/styled';
import { useState } from 'react';
import { EvenOdd } from './EvenOdd';
import { Heading } from './Heading';
import { IncrementButton } from './IncrementButton';
import { ResetButton } from './ResetButton';
import type { Empty, MayStyled, Styled, UnStyled } from 'types';

type Props = Empty;

const Hook = (_: Props) => {
  const [count, setCount] = useState(0);
  const increment = () => setCount((pre) => pre + 1);
  const reset = () => setCount(0);
  return { count, increment, reset };
};

const Component = (props: MayStyled<ReturnType<typeof Hook>>) => {
  const { count, increment, reset, className } = props as Styled<typeof props>;
  return (
    <div className={className}>
      <Heading count={count} />
      <EvenOdd className="even-odd" count={count} />
      <div className="buttons">
        <IncrementButton onClick={increment} />
        <ResetButton className="reset" onClick={reset} />
      </div>
    </div>
  );
};

const StyledComponent = styled(Component)`
  padding: 24px;
  > .even-odd {
    margin-top: 16px;
  }
  > div.buttons {
    margin-top: 16px;
    > .reset {
      margin-left: 16px;
    }
  }
`;

const FunctionalStyledComponent = (props: MayStyled<Props>) => {
  const { className, ...rest } = props;
  return <StyledComponent className={className} {...Hook(rest as UnStyled<typeof props>)} />;
};

export const Home = FunctionalStyledComponent;

pages/Heading.tsx

import styled from '@emotion/styled';
import type { MayStyled, Styled } from 'types';

type Props = {
  count: number;
};

const Component = (props: MayStyled<Props>) => {
  const { count, className } = props as Styled<typeof props>;
  return <h1 className={className}>Current count is {count}</h1>;
};

const StyledComponent = styled(Component)`
  font-size: large;
  font-weight: bold;
`;

export const Heading = StyledComponent;

pages/EvenOdd.tsx

import styled from '@emotion/styled';
import type { MayStyled, Styled, UnStyled } from 'types';

type Props = {
  count: number;
};

const Hook = (props: Props) => {
  const { count } = props;
  const isEven = count % 2 === 0;
  return { isEven };
};

const Component = (props: MayStyled<ReturnType<typeof Hook>>) => {
  const { isEven, className } = props as Styled<typeof props>;
  return (
    <p className={className}>
      This is
      <span className={isEven ? 'even' : 'odd'}>{isEven ? 'EVEN' : 'ODD'}</span>
      number.
    </p>
  );
};

const StyledComponent = styled(Component)`
  color: #666;
  > span {
    margin: 0 8px;
    &.even {
      color: #f00;
    }
    &.odd {
      color: #00f;
    }
  }
`;

const FunctionalStyledComponent = (props: MayStyled<Props>) => {
  const { className, ...rest } = props;
  return <StyledComponent className={className} {...Hook(rest as UnStyled<typeof props>)} />;
};

export const EvenOdd = FunctionalStyledComponent;

pages/IncrementButton.tsx

import styled from '@emotion/styled';
import type { MayStyled, Styled } from 'src/types';

type Props = {
  onClick: () => void;
};

const Component = (props: MayStyled<Props>) => {
  const { onClick, className } = props as Styled<typeof props>;
  return (
    <button className={className} onClick={onClick} type="button">
      Increment
    </button>
  );
};

const StyledComponent = styled(Component)`
  padding: 8px;
  font-size: small;
  font-weight: bold;
  color: #fff;
  background-color: #f0f;
  border-radius: 4px;
`;

export const IncrementButton = StyledComponent;

pages/ResetButton.tsx

import styled from '@emotion/styled';
import type { MayStyled, Styled } from 'src/types';

type Props = {
  onClick: () => void;
};

const Component = (props: MayStyled<Props>) => {
  const { onClick, className } = props as Styled<typeof props>;
  return (
    <button className={className} onClick={onClick} type="button">
      Reset
    </button>
  );
};

const StyledComponent = styled(Component)`
  padding: 8px;
  font-size: small;
  font-weight: bold;
  color: #000;
  background-color: #ff0;
  border-radius: 4px;
`;

export const ResetButton = StyledComponent;

以上 #

ご参考になりましたら幸いです。