こんにちは、21新卒エンジニアのTepyです。
PR TIMESでは自社サービスのプレスリリース配信プラットフォームのレガシー改善を行っており、その中の企業詳細ページのフロントエンドをReactにリプレイスするプロジェクトを行いました。
今回のリプレイスプロジェクトに限らず、新しいReactプロジェクトをゼロから作るたびに、ステート管理やAPIからのデータフェッチやテストなどのライブラリーを選択するのは悩ましいことだと思いますので、本プロジェクトに利用した便利なReactライブラリーを紹介したいと思います。もちろん、プロジェクトの規模や要望などの要因によって、ライブラリーの決断が変わると思いますが、今回の事例から何かの役に立つ情報になれればと思います。
事例に表したコードはDesign Systemの未完成や時間制限などの理由でリファクタリングの余地がまだありますが、今後リファクタリングする予定です。
Emotion
React Componentのスタイリング方法は一般的なCSSスタイリングやCSS Module
やCSS-in-JS
などがあると思いますが、今回はCSS-in-JS
を採用しました。もちろん、各方法は利点と欠点があって、どちらの方法も他の方法より完全に良いとは言えないと思いますので、今回のプロジェクトはコンポーネントのメンテナンスと開発スピードを注目しました。
- メンテナンス:CSSコードは各コンポーネントに直接に書き込まれているため、そのコンポーネントに影響があるCSSを探したいときに、すぐに見つかる
- 開発スピード:開発メンバー全員が
CSS-in-JS
を利用した経験があるため、キャッチアップしやすい
そこで、styled-components
やemotion
やlineria
などのCSS-in-JS
ライブラリーがある中、Emotionに決定しました。それぞれのライブラリーの機能がほぼ同じで(どのライブラリーもトレードオフがある)、開発メンバーの全員がemotionを利用した経験があるため、emotion
を採用しました。
本プロジェクトのEmotion事例
主にemotion
のcomposition 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 literal
のconcatenation
のやり方:
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-spring
、react-transition-group
、(framer)motion
などのアニメションライブラリーが存在します。その中に、react-spring
とmotion
はDeclarative
で使い勝手があるライブラリーで、Hooks
との相性も良いと考えられます。この2つからmotion
を選んだ理由としては、motion
を利用することでデザイナーがframer
で作成したアニメーション付きコンポーネント(検討中)をそのままにReact
コードを利用できる利点があるからです。
本プロジェクトのFramer Motion事例
- HTML要素の
motion
:motion.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つのルールがあります:motion
にwrap
されるコンポーネントは必ずアニメーションを与えたいDOM要素
にref
をforward
すること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
- Recoil:Meta(元Facebook)の新しい
state management
ライブラリー。Hooks
のapiに近い - Zustand:
flux
の原則を簡略化したsmall, fast and scalable
ライブラリー - Easy-peasy:DX(Developer Experience)を注目した
Redux
のabstraction
によるライブラリー - …
- Redux-ToolKit(RTK):状態管理ではないが、
その中から、ーつを選ぶのはなかなか迷うことですが、今回のプロジェクトにRecoilを選択した理由はいくつかありました。
- プロジェクト規模:今回の「企業詳細ページ」のリプレイスにはステート管理が少なく、
React-Redux
よりライトなuseState
に近いRecoil
の方がいい(Recoil
は複雑なステート管理でも大丈夫だと思います) - メンバーの経験:
React-Redux
以外に利用した、または趣味で学んだライブラリーはRecoil
でしたので、キャッチアップが早い(そもそもRecoil
はほぼuseState
の使い方と一緒なので)
もちろん、Recoilは新しいライブラリーなので欠点もあります。まずは、Recoil
のDocument
ページにコードイグザンプルや使い方の説明が記載されていますが、それ以上のリソースはまだ少ないだと思います。また、Recoil
のGitHub 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
モック、テストなどのライブラリーも次回紹介したいと思います。