新規Reactプロジェクトに便利なReact Libraryを紹介

  • URLをコピーしました!

こんにちは、21新卒エンジニアのTepyです。

PR TIMESでは自社サービスのプレスリリース配信プラットフォームのレガシー改善を行っており、その中の企業詳細ページのフロントエンドをReactにリプレイスするプロジェクトを行いました。

今回のリプレイスプロジェクトに限らず、新しいReactプロジェクトをゼロから作るたびに、ステート管理やAPIからのデータフェッチやテストなどのライブラリーを選択するのは悩ましいことだと思いますので、本プロジェクトに利用した便利なReactライブラリーを紹介したいと思います。もちろん、プロジェクトの規模や要望などの要因によって、ライブラリーの決断が変わると思いますが、今回の事例から何かの役に立つ情報になれればと思います。

事例に表したコードはDesign Systemの未完成や時間制限などの理由でリファクタリングの余地がまだありますが、今後リファクタリングする予定です。

目次

Emotion

React Componentのスタイリング方法は一般的なCSSスタイリングやCSS ModuleCSS-in-JS などがあると思いますが、今回はCSS-in-JS を採用しました。もちろん、各方法は利点と欠点があって、どちらの方法も他の方法より完全に良いとは言えないと思いますので、今回のプロジェクトはコンポーネントのメンテナンスと開発スピードを注目しました。

  • メンテナンス:CSSコードは各コンポーネントに直接に書き込まれているため、そのコンポーネントに影響があるCSSを探したいときに、すぐに見つかる
  • 開発スピード:開発メンバー全員がCSS-in-JS を利用した経験があるため、キャッチアップしやすい

そこで、styled-componentsemotionlineriaなどのCSS-in-JSライブラリーがある中、Emotionに決定しました。それぞれのライブラリーの機能がほぼ同じで(どのライブラリーもトレードオフがある)、開発メンバーの全員がemotionを利用した経験があるため、emotionを採用しました。

プロジェクトのEmotion事例

主にemotioncomposition css props 機能を利用しました。

  • 普通のコンポーネントスタイル
const link = css`
  color: ${colors.text.link};
  ...(略)
`;

const Link = ({
  to,
  ...props
}: Props): JSX.Element => {
  return (
    <a css={link} to={to} {...props}>
      {children}
    </a>
  );
};
  • Composableスタイリング

string literalconcatenation のやり方:

const common = css`
  display: flex;
  justify-content: center;
  align-items: center;
  ...(略)
`;

export const Button = ({
  variant = 'fill',
  color = 'primary',
  size = 'medium',
  children,
  ...props
}: Props) => {
  const button = css`
    ${common};
    ${variantThemes[variant]};
    ${colorThemes[variant][color]};
    ${sizeThemes[variant][size]};
  `;

  return (
    <button css={button} {...props}>
      {children}
    </button>
  );
};

配列のやり方:

const scrollTop = css`
  visibility: hidden;
  opacity: 0;
  ....(略)
`;

const ScrollTopButton = () => {
  const { scrollY } = useWindowScroll();

    const activeStyle =ss`
    opacity: 1;
    visibility: visible;
  `;

  return (
    <button
      css={[scrollTop, scrollY > SCROLL_THRESHOLD && activeStyle]}
      onClick={() => {
        window.scrollTo({ top: 0, behavior: 'smooth' });
      }}
      title='ページトップへ戻る'
    >
      <ScrollTopIcon />
    </button>
  );
};

今回のリプレイスプロジェクトではDesign Systemは範囲外になったため、tailwindscsss のようなutility css ライブラリーは選択肢になっていないが、今後 tailwindscss を利用してDesign System を作るのも考えています。

Framer Motion

本プロジェクトではまだ複雑的なアニメションがないですが、何箇所でslide などのアニメションをmotion というアニメションライブラリーで実装しました。

Reactの世界には、react-springreact-transition-group(framer)motion などのアニメションライブラリーが存在します。その中に、react-springmotionDeclarativeで使い勝手があるライブラリーで、Hooksとの相性も良いと考えられます。この2つからmotionを選んだ理由としては、motionを利用することでデザイナーがframerで作成したアニメーション付きコンポーネント(検討中)をそのままにReactコードを利用できる利点があるからです。

プロジェクトのFramer Motion事例

  • HTML要素のmotionmotion.div,motion.header
export const slideAnimation = {
  initial: {
    opacity: 0,
    top: '-100px',
  },
  animate: {
    opacity: 1,
    top: '0',
  },
  exit: {
    opacity: 0,
    top: '-100px',
  },
  transition: {
    duration: 0.4,
    ease: 'easeOut',
  },
};
const ScrolledHeader = () => {
  return (
    <AnimatePresence>
      <motion.header
        data-testid='scrolled-header'
        {...slideAnimation}
        css={header}
      >
        <div css={container}>
          ....(略)
        </div>
      </motion.header>
    </AnimatePresence>
  );
}
  • custom component:Reactのコンポーネントと利用する場合、2つのルールがあります:
    • motionwrapされるコンポーネントは必ずアニメーションを与えたいDOM要素refforwardすること
    • motion()関数はReactのrender関数の中に呼び出さないこと

詳しいはこちらの参考に→

const modalMotion = {
  initial: {
    opacity: 0,
    y: -320,
  },
  animate: {
    opacity: 1,
    y: 0,
  },
  exit: {
    opacity: 0,
    y: -320,
  },
  transition: { type: 'tween' },
};

function refOverlay(props: OverlayProps, ref: ForwardedRef<HTMLDivElement>) {
  return <Overlay forwardRef={ref} {...props} />;
}

const MotionOverlay = motion(forwardRef(refOverlay), {
  forwardMotionProps: true,
});

export const Modal = ({
  variant = 'default',
  width,
  isShow,
  device = 'pc',
  close,
  children,
  ...props
}: Props): JSX.Element => {
  ....(略)
  return (
    <AnimatePresence>
      {isShow && (
        <MotionOverlay
          onClick={close}
          {...overlayMotion}
          data-testid='modalOverlay'
        >
          <div css={modalWrapper}>
            <motion.div
              css={[modal, modalVariant]}
              onClick={(e) => e.stopPropagation()}
              {...modalMotion}
              {...props}
            >
              <div css={closeWrapper}>
                <button onClick={close}>
                  <CloseCircleIcon variant='gray' />
                </button>
              </div>
              <div css={content}>{children}</div>
            </motion.div>
          </div>
        </MotionOverlay>
      )}
    </AnimatePresence>
  );
};

Recoil

各Reactプロジェクトの初段階での悩むのはstate managementライブラリーの選択だと自分は思っています。Reactのバージョンアップと同様に、どんどんstate managementライブラリーが生み出されています。既存と最近のライブラリーをリストアップすると:

  • 既存(the-old-school
    • React-Redux
    • MobX
    • MobX State Tree(MST)
    • React-Context
  • 最近(the-new-cool-kids
    • Redux-ToolKit(RTK):状態管理ではないが、react-reduxをシンプルに利用できるツールキット
    • React-Query:こちらもdata-fetchingライブラリーの方が適切ですが、一部は状態管理にも対応できる(データ取得するときのloading、データキャッシュなどの状態を管理できる)
    • XState:JavaScriptとTypeScriptの有限オートマトンとstatecharts
    • RecoilMeta(元Facebook)の新しいstate managementライブラリー。Hooksのapiに近い
    • Zustandfluxの原則を簡略化したsmall, fast and scalableライブラリー
    • Easy-peasy:DX(Developer Experience)を注目したReduxabstractionによるライブラリー

その中から、ーつを選ぶのはなかなか迷うことですが、今回のプロジェクトにRecoilを選択した理由はいくつかありました。

  • プロジェクト規模:今回の「企業詳細ページ」のリプレイスにはステート管理が少なく、React-ReduxよりライトなuseStateに近いRecoilの方がいい(Recoilは複雑なステート管理でも大丈夫だと思います)
  • メンバーの経験:React-Redux以外に利用した、または趣味で学んだライブラリーはRecoilでしたので、キャッチアップが早い(そもそもRecoilはほぼuseStateの使い方と一緒なので)

もちろん、Recoilは新しいライブラリーなので欠点もあります。まずは、RecoilDocumentページにコードイグザンプルや使い方の説明が記載されていますが、それ以上のリソースはまだ少ないだと思います。また、RecoilGitHub Repo のタイトルに「Recoil is an experimental state management library for React apps. 」と書いてある通り、まだexperimentalなのでbugやAPI変更などによる不具合が発生するかもしれませんので、この点も考慮する必要があります。

Recoilを利用すると言っても、実際にRecoilでステートを管理する箇所が2つ、3つしかなかったです。それは今回の実装した企業詳細ページUIの複雑さによります。今回の実装ではglobalに管理必要なステートが少なく、globalに管理必要のないステートはコンポーネント内のステートとしてで実装しました(ここはベストプラクティスだと思います)。

今回はglobalステートが少ないため、ある程度のRecoilの強さ(または欠点)しか触っていないと思いますので、今後の新機能追加などの実装でglobalステートがある程度複雑になるときに、またRecoilに関しての考察をしたいと思います。

本プロジェクトのRecoil事例

import { atom } from 'recoil';

export const currentTabState = atom({
  key: 'currentTabState',
  default: 0,
});

export const pressReleasesState = atom<PressReleasesByCompanyId[] | undefined>({
  key: 'pressReleasesState',
  default: [],
});

export const pressReleasesCountState = selector({
  key: 'pressReleasesCountState',
  get: ({ get }) => {
    const pressReleases = get(pressReleasesState);
    if (!pressReleases || pressReleases.length === 0) return 0;
    return pressReleases[0].total;
  },
});

const CompanyTab = forwardRef<TabRef, CompanyTabProps>(
  ({ company, ...props }, ref): JSX.Element => {
    const [currentTab, setCurrentTab] = useRecoilState(currentTabState);
    const totalPressReleasesCount = useRecoilValue(pressReleasesCountState);
    const currentPressReleasesCount = useRecoilValue(
      currentPressReleasesCountState,
    );
    const storiesCount = useRecoilValue(storiesCountState);
    ....(略)
    return (
      <Tabs
        defaultIndex={0}
        index={currentTab}
        onChange={(index) => setCurrentTab(index)}
        ref={ref}
        data-testid='company-tab'
        {...props}
      >
        ....(略)
        <TabList css={tabList}>
            <Tab css={tab}>
              <TabTitle
                index={0}
                title='プレスリリース'
                count={totalPressReleasesCount}
              />
            </Tab>
            <Tab css={tab}>
              <TabTitle index={1} title='ストーリー' count={storiesCount} />
            </Tab>
        </TabList>
        ....(略)
      </Tabs>
    );
}

まとめ

今回のプロジェクトはゼロから作るということで、便利な最先端の技術を利用できてとても良かったと思います。やはり優れるライブラリーを利用すると、開発のスピードもある程度上がり、プロジェクト全体も進めやすいと感じました。

また、今回はスタイリング、アニメションと状態管理ライブラリーの選択理由と使い方を紹介できたと思いますが、データフェッチ、apiモック、テストなどのライブラリーも次回紹介したいと思います。

  • URLをコピーしました!

この記事を書いた人

PR TIMESのフロントエンドエンジニアです。
主にReactを書いています。最先端Frontend技術を試すのが好きです。
@TepyThai

目次