おはこんばんちは、@ちーずです。
アドベントカレンダーもやっとこさ1週間ですね!!
本日のテーマは、「よく使うhooks・Component集」です!!
ライブラリ的なものをひたすら紹介するだけの記事です。
hooks
react-use
便利なhooks寄せ集め、みたいなもの。
チュートリアルやコードの例で見る**useCounter
や
最初の呼び出しを無視するuseEffect
が実装されているuseUpdateEffect
**など、
自分でかくとちょっとめんどくさいような、かゆいところに手が届くhooksがたくさん入っています。
react-hook-form
ありとあらゆるフォーム周りのStateを取り使えるhooks。
フォームって自前で実装すると、結構大変ですよね。
react-hook-form
では、バリデーションエラーやエラー処理はもちろんのこと、
入力値を検知して出し分けしたり、変更時に処理を走らせたりなど、結構柔軟に表現することができます。
ただ、個人的に、ちょっと難しいです。。
Component
基本的にUIフレームワークを使わない前提で紹介します!
Selector - React Select
通常のセレクターだけではなく、
複数選択できるセレクターや、タグ表示にできるセレクターなど、
さまざまなパターンのセレクターが表現できます。
CSSも結構自由自在に書くことができるので、おすすめです。
Modal - react-modal
Reactのコミュニティが提供してくれているモーダル。
必要最低限且つシンプルな機能が兼ね備えられているため、中央に表示するモーダル
スタイルもオブジェクトスタイルで拡張可能。
(中央以外のレイアウトはちょっとめんどくさかった)
Tab - react-tabs
Reactのコミュニティが提供してくれているタブ。
モーダル同様、必要最低限且つシンプルな機能が兼ね備えられている。
ちゃんとrole
の指定もできる
Transition - react-transition-group
またまたReactのコミュニティが提供してくれているアニメーションを簡単に実装できるComponent。
Accordion
正直あまり良いプラグインはなかったので自前で実装しました。
Stateによってheaderのスタイルが変わる(+
→ -
みたいな)ことを実現したかったため、
headerとbodyに開閉状態を配布するプロバイダーで囲っています。
headerやbodyは上から自由にスタイルを拡張できるよう作りました。
サンプルコード
import { useReducer, createContext, DispatchWithoutAction, FC } from 'react';
export type AccordionContextType = {
isOpen: boolean;
toggleAccordion: DispatchWithoutAction;
accordionName: string;
};
export const AccordionContext = createContext<AccordionContextType>({
isOpen: false,
toggleAccordion: () => {
// no-op
},
accordionName: ''
});
export type AccordionProviderProps = {
initialIsOpen: boolean;
};
export const AccordionProvider: FC<AccordionProviderProps> = ({
children,
initialIsOpen,
accordionName
}) => {
const [isOpen, toggleAccordion] = useReducer(
(state) => !state,
initialIsOpen
);
return (
<AccordionContext.Provider value={{ isOpen, toggleAccordion, accordionName }}>
{children}
</AccordionContext.Provider>
);
};
export const useAccordionContext = (): AccordionContextType => {
return useContext<AccordionContextType>(AccordionContext);
};
import React, { VFC, ReactNode } from 'react';
import { useAccordionContext } from 'AccordionProvider';
type Props = {
children: ReactNode;
} & JSX.IntrinsicElements['div'];
export const AccordionHeader: VFC<Props> = ({
children,
...attrs
}) => {
const { isOpen, toggleAccordion, accordionName } = useAccordionContext();
return (
<div
onClick={toggleAccordion}
aria-controls={accordionName}
aria-expanded={isOpen}
{...attrs}
>
{children}
</div>
);
};
import React, { FC, ReactNode } from 'react';
import { useAccordionContext } from 'AccordionProvider';
import { CSSTransition } from 'react-transition-group';
type Props = {
children: ReactNode;
} & JSX.IntrinsicElements['div'];
export const AccordionHeader: FC<Props> = ({
children,
...attrs
}) => {
const { isOpen, accordionName } = useAccordionContext();
// アニメーション周り色々している
const { bodyElm, transitionMethod } = useAccordionBody();
return (
<CSSTransition in={isOpen} timeout={duration} {...transitionMethod}>
<div
id={accordionName}
aria-hidden={!isOpen}
ref={bodyElm}
css={styles.container(duration)}
{...attr}
>
{children}
</div>
</CSSTransition>
);
};
Rating Star
これもスター数が多いものがなく、心折れながらもなんとか実装しました。
なかなか強引な方法ですが、svgのグラデーションを用いて実現しています。
(拡張性はあまりないです。)
サンプルコード
import React, { FC } from 'react';
import { RatingStarIcon } from './RatingStarIcon';
import { css } from '@emotion/react';
type Props = {
size: number;
score: number;
id: string;
activeColor: string;
disabledColor: string;
gap?: SizeValues;
max?: number;
} & JSX.IntrinsicElements['div'];
const styles = {
container: (gap: number) => css`
display: flex;
> * {
&:not(:last-child) {
margin-right: ${gap / 10}rem;
}
}
`,
};
export const RatingStars: FC<Props> = ({
size,
score,
gap = '2px',
id,
activeColor.,
disabledColor,
max = 5,
...attrs
}) => {
const starRate = (count: number, score: number): number => {
if (score - count > 1) return 100;
if (score - count > 0) return (score - count) * 100;
return 0;
};
return (
<div css={styles.container(gap)} {...attrs}>
{[...Array(max)].map((_, index) => {
const rate = starRate(index, score);
return (
<React.Fragment key={`star-${index}`}>
<RatingStarIcon
id={id}
width={size}
activeColor={activeColor}
activeColor={disabledColor}
height={size}
rate={rate}
index={index}
/>
</React.Fragment>
);
})}
</div>
);
};
import React, { FC } from 'react';
type Props = {
id: string;
rate: number;
index: number;
activeColor: string;
disabledColor: string;
width?: number;
height?: number;
};
export const RatingStarIcon: FC<Props> = ({
id,
width = 30,
height = 30,
activeColor,
disabledColor,
index,
rate,
}) => {
return (
<svg
width={width}
height={height}
viewBox="0 0 30 30"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient
id={`gradient${index}_${id}`}
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0%" stopColor={activeColor} />
<stop offset={`${rate}%`} stopColor={activeColor} />
<stop offset={`${rate}%`} stopColor={disabledColor} />
<stop offset="100%" stopColor={disabledColor} />
</linearGradient>
</defs>
<path
fill={`url(#gradient${index}_${id})`}
d="M15 24.1105L24.27 30L21.81 18.9L30 11.4316L19.215 10.4684L15 0L10.785 10.4684L0 11.4316L8.19 18.9L5.73 30L15 24.1105Z"
/>
</svg>
);
};