- 投稿日:2020-08-05T22:53:23+09:00
きじくんの備忘録 ~React編①
はじめに
これは,フロントエンジニアを目指している僕が勉強の過程をメモしていく記事です.
一応,僕自身の為に書いていますが,誰かの役に立ったら幸いです!今回はJavaScriptライブラリであるReactの備忘録第1回です.
コンポーネント
コンポーネントとは見た目と機能を足し合わせた,言わばWebサービスのページの部品に当たるもの.Reactはこのコンポーネントを再利用することで,コード量を減らすことができる.
仮想DOM
React内で管理するDOM
普通のDOMsample.jsdocument.getElementById('foo').innerText='bar'仮想DOM
sample.jsxrender( <div id='foo'>bar</div> )JSX
JavaScript内部でHTMLを書ける.
sample.jsxReactDOM.render( <div className={foo}> <h1>bar</h1> </div> )JSXの基礎文法
sample.jsximport React from 'react'; // HTMLと同様に書ける.ただし,divタグで全体を囲うなどのしないとエラーが出る. const App=()=>{ return( <div className={foo}> <h1>bar</h1> </div> ); }; // {}内に変数や関数を埋め込める. const foo = '<p>FOO!</p>' const App=()=>{ return( <div id='bar' className={baz}> {foo} </div> ); }; // 空要素は閉じる const App=()=>{ return( <div className={foo}> <input type="text" id="bar"/> <img src="/img/baz.png" /> </div> ); };あとがき
今回はReactの要素のJSXについて主に書きました.今後も少しずつ,Reactに関する備忘録を書いていくのでよろしくお願いします.
参考文献
- 投稿日:2020-08-05T21:28:22+09:00
React HooksでStackな構造を扱うカスタムフック
スタック構造を持ったデータをスッキリ扱いたかったので作った。
作ってみてわかったのは、これはもうほとんどArrayにスタックのインターフェースを被せただけものになったということ。
それでも使う側はだいぶスッキリするから、ないよりはあった方が良くはある。useStackRef.tsimport { useRef } from "react"; export interface Stack<T> { value: T | null size: number push: (t: T) => number pop: () => T reset: () => void clear: () => void } export const useStackRef = <T>(init: T | T[] | (() => T) | (() => T[]) = []): Stack<T> => { const initial: T[] = ((value) => Array.isArray(value) ? value : [value])(init instanceof Function ? init() : init) const stackRef = useRef<T[]>(initial) const push = (t: T): number => stackRef.current.push(t) const pop = (): T => stackRef.current.pop() || null const reset = (): void => { stackRef.current = initial } const clear = (): void => { stackRef.current = [] } return { value: stackRef.current.slice(-1)[0] || null, size: stackRef.current.length, push, pop, reset, clear }; };ちなみに、useRefでなくuseStateのスタックを作って紹介している記事はこちら。
React Hooksのカスタムフックが実現する世界 - オブジェクト指向とOSS
- 投稿日:2020-08-05T21:20:24+09:00
WordPress製ホームページをGatsby+microCMS+Netlifyに移行してみた
プライベートでやっているバンドのホームページをレンタルサーバー+WordPressで運営していたのですが、最近Gatsby+microCMS+NetlifyのいわゆるJAMstack構成のホームページにリプレースしました。
ホームページ:男子校 Official Web Site
概要
今までかれこれ5年くらいレンタルサーバー+WordPressで運営してきていたのですが、
- サーバー代が毎年かかる
- 表示が重い
- 「固定ページやフォームをちょっとだけ動的にしたい」みたいなことが難しい
- プラグイン・バージョンのアップデートといった管理が面倒
といった点が課題でした。
これらの課題を解消したく、また最近気になっていたGatsbyを使ってみたかったのもあってホームページをリプレースしてみました。
構成
TypeScript+React+Gatsbyでコードを作成、microCMSでデータを管理してNetlifyでホスティングするという構成にしました。いわゆるJAMstackと呼ばれている構成で、動的なWebサーバーを必要とせず静的コンテンツを高速配信することができます。
Gatsby
React製のSSG(静的サイトジェネレーター)。MarkdownファイルやHeadless CMSと組み合わせてブログ等のサイトを簡単に作成でき、かつ爆速なサイトが簡単に作れると評判です。
「静的」とは言っていますがReactの機能はフルに使えるので、フロントエンドをフルに活用したサイトを作ることができます。microCMS
日本製のHeadless CMS。
個人的には管理画面がシンプルで一番使いやすそうと感じたので採用しました。特にバンドのホームページは自分以外も更新するので、直感的に記事を作成できるのは大きなメリットです。もちろん全て日本語なのもGood。
Gatsbyには対応プラグインがあるので、これを使用することでGraphQLクエリで簡単にデータを取得できます。
無料でも10個までAPIを作成できるので、ブログ記事以外にも曲情報やライブ情報といったAPIを作成してホームページ内で利用しています。Netlify
デプロイ先の静的ホスティングサービス。静的サイト・サーバーレスに特化したサービスとなっていて、以下のような機能を提供してくれます。
- GitHubやHeadless CMSと連携した自動ビルド(プライベートリポジトリでも可)
- フォーム機能
- AWS Lambdaベースのfunctions機能
microCMSにはNetlifyとWebhook連携できる機能があるので、microCMSで記事の作成・更新時にNetlifyに通知し、更新後のコンテンツで自動ビルドすることができます。
効果
リプレースによって上にあげた課題がかなり改善しました。
- サーバー代が毎年かかる
- → Netlify / microCMSともにに無料枠で十分なのでサーバー代は0になり、ドメイン代だけで済むようになった
- 表示が重い
- → GatsbyやNetlify CDNのおかげでかなり爆速に
- 「固定ページやフォームをちょっとだけ動的にしたい」みたいなことが難しい
- → Reactをフルに利用できるので動的処理も自由に入れられる
- 例えば「ライブ情報から、未来の日付のライブだけ予約できるフォームを自動で作る」のようなこともWordPressでは難しかったのが簡単にできた
- プラグイン・バージョンのアップデートといった管理が面倒
- → GitHub / npmで一括管理1
- セキュリティアップデートなんかはGitHubが自動でプルリクを生成してくれたりする
Gatsbyの使い方もReactが分かっていれば割ととっつきやすく2、Gatsbyを初めて触ってからだいたい1ヶ月くらいでリプレースできました。
ポイント
主にGatsbyまわりのちょっとしたポイントやハマった点をいくつか紹介しようと思います。
(具体的な環境構築の手順等は今回はあまり触れません)デザインライブラリ
1からデザインを作成するのは結構大変だったので、Reactのマテリアルデザインライブラリmaterial-uiを使用しました。
Gatsbyで使用する場合は、gatsby-theme-material-uiプラグインを使用すればよいです。
src/gatsby-theme-material-ui-top-layout/theme.ts
を作成することで以下のようにテーマをカスタマイズできます。src/gatsby-theme-material-ui-top-layout/theme.tsimport { createMuiTheme } from '@material-ui/core' import { indigo } from '@material-ui/core/colors' const theme = createMuiTheme({ palette: { primary: { main: '#fbfb99' }, secondary: { main: indigo.A400 }, }, props: { MuiLink: { color: 'secondary' }, MuiTextField: { variant: 'outlined', color: 'secondary' }, }, }) export default themeTypeScript対応
GatsbyはデフォルトでTypeScript対応していて、
tsconfig.json
等でカスタマイズすることもできます。
また、gatsby-plugin-graphql-codegen
プラグインを使用することでGraphQLの取得データにも型補完が効いてかなり開発しやすくなります。
gatsby-plugin-graphql-codegen
を使用する場合は以下の設定が個人的におすすめです。Gatsby+TypeScriptを快適にするためのgatsby-plugin-graphql-codegenの設定
microCMSデータ取得
GatsbyとmicroCMSを連携するには先ほど述べた通り対応プラグインgatsby-source-microcmsを利用できます。
microCMSはコンテンツごとに複数のAPIを作成できますが、その場合は以下のようにgatsby-config
でプラグインを複数回読み込むことでそれぞれデータ取得できます。gatsby-config.tsimport { ITSConfigFn } from 'gatsby-plugin-ts-config' const gatsbyConfig: ITSConfigFn<'config'> = ({ projectRoot }) => ({ plugins: [ // 中略 { resolve: 'gatsby-source-microcms', options: { apiKey: process.env.GATSBY_MICROCMS_API_KEY, // APIキーは環境変数で管理するのが良いです serviceId: process.env.GATSBY_MICROCMS_ENDPOINT, endpoint: 'index', // APIのエンドポイント format: 'object', // オブジェクト形式の場合 }, }, { resolve: 'gatsby-source-microcms', options: { apiKey: process.env.GATSBY_MICROCMS_API_KEY, serviceId: process.env.GATSBY_MICROCMS_ENDPOINT, endpoint: 'blogs', // APIのエンドポイント format: 'list', // リスト形式の場合 readAll: true, // リスト形式の場合デフォルトでは最新10件のみ取得するので全件取得したい場合はこれをtrueにする }, }, // ... ] }) export default gatsbyConfigmicroCMSのリッチエディタ(HTML)→JSX変換
microCMSで使用できるリッチエディタはHTMLをGUIエディタで作成できるのですが、APIでは生のHTML文字列として取得されます。
HTML文字列をReactでレンダリングするにはconst BlogPost: React.FC<{ body: string }> = ({ body }) => ( <div dangerouslySetInnerHTML={{ __html: body }} /> )のようにすればよいのですが、これだとHTMLそのままで出力されてしまうのでスタイルを適用するのがやや大変です。特にmaterial-uiのようなReactコンポーネントのライブラリの場合はこのままだと難しいです。
このHTMLにmaterial-uiのデザインを適用させるために、rehype-reactというライブラリでHTML→JSXに変換しました。まず、以下のように各タグ(以下は
h1
を想定)に対応するコンポーネントを作成します。Typography.tsximport React from 'react' import { Box, Typography, Divider } from '@material-ui/core' type Props = { id?: string; className?: string } export const Chapter: React.FC<Props> = ({ children, className, ...props }) => ( <Box mt={2} mb={1} className={className}> <Typography {...props} variant="h4" component="h2"> {children} </Typography> <Divider /> </Box> ) // ... 略そして、
rehype-parse
でHTMLをパース・rehype-react
でHTMLタグをJSXコンポーネントに変換するrender
関数を作成します。render.tsximport React from 'react' import unified from 'unified' import parser from 'rehype-parse' import rehypeReact from 'rehype-react' import { Chapter } from '../components/Typography' const processor = unified() .use(parser, { fragment: true }) .use(rehypeReact, { createElement: React.createElement, components: { h1: Chapter, // ...以下タグとコンポーネントの対応を並べていく }, }) // HTML -> JSX に変換する関数 export const render = (html: string) => processor.processSync(html).resultこれをReactコンポーネント内で
const BlogPost: React.FC<{ body: string }> = ({ body }) => ( <div>{render(body)}</div> )といった感じで使用することで、microCMSから取得したHTMLにmaterial-uiのスタイルを適用してレンダリングできます。
フォーム
Netlify Formsを利用すると静的サイトでもフォーム機能を利用することができます。
基本は<form>
タグのアトリビュートにnetlify
かdata-netlify="true"
と記載するだけで使用できるのですが、Gatsbyのようにビルド時にHTMLを生成する場合はNetlifyがビルド時に上記アトリビュートを読み取れないので、以下のようにname="form-name", value=(formのnameの値)
と置いたinput
タグを挿入する必要があります。const Form: React.FC = () => ( <form name="contact" method="POST" data-netlify="true"> <input type="hidden" name="form-name" value="contact" /> {/* この行を追加 */} <p> <label>Your Name: <input type="text" name="name" /></label> </p> <p> <label>Your Email: <input type="email" name="email" /></label> </p> <p> <label>Your Role: <select name="role[]" multiple> <option value="leader">Leader</option> <option value="follower">Follower</option> </select></label> </p> <p> <label>Message: <textarea name="message"></textarea></label> </p> <p> <button type="submit">Send</button> </p> </form> )また、ReCAPTCHAを利用する場合も
form
タグにdata-netlify-recaptcha="true"
を記述したうえで<div data-netlify-recaptcha="true"></div>
という空のdiv
タグを置けばNetlify側でReCAPTCHAを作成してくれるのですが、これもGatsbyでは読み取ってくれないので、react-google-recaptcha
等のライブラリで実装する必要があります。この辺は以下のようにラップしたフォームコンポーネントを作成しておくのが便利です。
Form.tsximport React from 'react' import { makeStyles, Button } from '@material-ui/core' import ReCAPTCHA from 'react-google-recaptcha' const useStyles = makeStyles(theme => ({ button: { marginTop: theme.spacing(2) }, })) type Props = { name: string action: string className?: string } const Form: React.FC<Props> = ({ children, name, action, className }) => { const classes = useStyles() return ( <form name={name} action={action} method="POST" data-netlify="true" data-netlify-recaptcha="true" className={className} > <input type="hidden" name="form-name" value={name} /> {children} <ReCAPTCHA sitekey={process.env.GATSBY_SITE_RECAPTCHA_KEY ?? ''} /> <Button type="submit" variant="outlined" className={classes.button}> 送信 </Button> </form> ) } export default Formビルド時・フォーム受信時等に通知する
Netlify Functionsにはビルドやフォーム受信等のイベントに反応して処理をトリガーする機能があります。
Trigger serverless functions on events特定のファイル名(デプロイ成功時なら
deploy-succeeded.js
等)でfunctionを作成することで、ビルド完了時にSlackに通知するといった連携機能を簡単に作成できます。funcions/deploy-succeeded.js// 例:デプロイ成功時にSlackに投稿する // Slack App の作成方法等は省略 require('dotenv').config() const { WebClient } = require('@slack/web-api') const slack = new WebClient(process.env.SLACK_TOKEN) exports.handler = async (event, context) => { const { deploy_time } = JSON.parse(event.body).payload try { await slack.chat.postMessage({ text: `ホームページが更新されました! (ビルド時間 : ${deploy_time} 秒)`, channel: process.env.SLACK_CHANNEL_INFO, }) return { statusCode: 200, body: 'ok' } } catch (error) { console.error(error) return { statusCode: 500, body: 'Internal server error' } } }まとめ
ホームページをWordPressからGatsbyに移行して幸せになったお話と、ちょっとしたポイント集でした(まとまりがない…)。
具体的な構築方法は今回はあまり触れなかったのですが、Gatsbyは最近話題のフレームワークということもあって結構ドキュメント・記事が充実している印象で、調べながら割と簡単にホームページを作れました(例:gatsby タグの記事一覧 - Qiita)。
公式ドキュメントは今のところ英語だけですが、日本語訳も現在作成中(2020/08/05現在)のようなので楽しみです。
gatsbyjs/gatsby-ja: Japanese translation of Gatsbyjs.org
- 投稿日:2020-08-05T15:15:53+09:00
React 関数コンポーネントでスクロールイベントを実装するには?
Reactのfunctionコードでスクロールイベント等を実装すると、
イベント関数をuseCallback
でくくってメモ化しておかないとremoveEventListener
が働かないとか、
スクロールで使用するフラグはuseRef
で再レンダリングされないようにする...など、
意外と気に掛ける点が多かったので、備忘録も込めてコードを載せておきます。import React, { useState, useEffect, useRef, useCallback, } from 'react' const TestDom = () => { const [isDisplay, setIsDisplay] = useState(false) const isRunning = useRef(false) // スクロール多発防止用フラグ // リスナに登録する関数 const isScrollToggle = useCallback(() => { if (isRunning.current) return isRunning.current = true const scrollTop = window.pageYOffset || document.documentElement.scrollTop requestAnimationFrame(() => { if (scrollTop > 100) { setIsDisplay(true) } else { setIsDisplay(false) } isRunning.current = false }) }, []) // 登録と後始末 useEffect(() => { document.addEventListener('scroll', isScrollToggle, { passive: true }) return () => { document.removeEventListener('scroll', isScrollToggle, { passive: true }) } }, []) // バツボタンでリスナ削除~ などはこのように const onClickClose = () => { document.removeEventListener('scroll', isScrollToggle, { passive: true }) setIsDisplay(false) }
- 投稿日:2020-08-05T00:41:46+09:00
React.memo, useCallback, useMemoを理解する
メモ化
React.memo, useCallback, useMemoに共通する目的はメモ化である。
メモ化とは広範な言い方をするとキャッシュのこと。React.memo
ラップした関数コンポーネントの初回レンダー結果を記憶し、次回以降同じpropsが与えられた時に、記憶した結果を使用することで、無駄なレンダーをスキップできパフォーマンスを向上させることができる。
React.memoを使用しない場合
const CounterItem = ({ count, label }) => { console.log(`${label}がレンダリング`); return <div>{label}: {count}</div>; }; const CounterList = () => { const [count1, setCount1] = useState(0); const increment1 = () => setCount1(c => c + 1); const [count2, setCount2] = useState(0); const increment2 = () => setCount2(c => c + 1); return ( <> <button onClick={increment1}><CounterItem count={count1} label="左" /></button> <button onClick={increment2}><CounterItem count={count2} label="右" /></button> </> ); };左右ボタンのどっちを押しても、CounterListコンポーネント内のstateが更新されるので左右のCounterItemコンポーネントを両方再レンダーしてしまう。左ボタンを押した場合は数字が変更される左のCounterItemコンポーネントのみレンダーしたい。右のコンポーネントは変更されてないのでレンダーの必要がない。
React.memoを使用する場合
// React.memoで関数コンポーネントをラップ const CounterItem = React.memo(({ count, label }) => { console.log(`${label}がレンダリング`); return <div>{label}: {count}</div>; }); const CounterList = () => { const [count1, setCount1] = useState(0); const increment1 = () => setCount1(c => c + 1); const [count2, setCount2] = useState(0); const increment2 = () => setCount2(c => c + 1); return ( <> <button onClick={increment1}><CounterItem count={count1} label="左" /></button> <button onClick={increment2}><CounterItem count={count2} label="右" /></button> </> ); };propsが変更されたコンポーネントしかレンダーされない。
useCallback
第一引数に渡したコールバック関数をメモ化する。第二引数に渡した値の配列が変更された場合のみ、コールバック関数が作り直される。React.memoと併用することでパフォーマンスを向上させることができる。
useCallbackを使用しない場合
const CounterItem = React.memo(({ count, label, onClick }) => { console.log(`${label}がレンダリング`); return <button onClick={onClick}><div>{label}: {count}</div></button>; }); const CounterList = () => { const [count1, setCount1] = useState(0); const increment1 = () => setCount1(c => c + 1); const [count2, setCount2] = useState(0); const increment2 = () => setCount2(c => c + 1); return ( <> <CounterItem count={count1} label="左" onClick={increment1} /> <CounterItem count={count2} label="右" onClick={increment2} /> </> ); };左ボタンを押した場合、左右のCounterItemコンポーネントを両方再レンダーしてしまう。CounterItemコンポーネントをReact.memoでラップしてるのでpropsの変更がないコンポーネント(右ボタン)はレンダーをスキップして欲しいができていない。
上記の問題は参照の同一性が関係している。
"a" === "a" // true 1 === 1 // true const a = () => {} const b = () => {} // 同じ処理であってもfalseが返る a === b //falsestateの更新によりCounterListコンポーネントの再レンダリングが起きると、CounterListコンポーネント内のincrement1関数や increment2関数が再度定義される。再定義された関数は上記で示したように同じ参照ではなくなる。つまりCounterItemコンポーネントに渡す関数が同じ参照にはならない。よってReact.memoでラップしてもporpsの参照が違うのでレンダーがスキップされない。
useCallbackを使用する場合
useCallbackを使用することで上記の問題が解決される
const CounterItem = React.memo(({ count, label, onClick }) => { console.log(`${label}がレンダリング`); return <button onClick={onClick}><div>{label}: {count}</div></button>; }); const CounterList = () => { const [count1, setCount1] = useState(0); const increment1 = useCallback(() => setCount1(c => c + 1), []); const [count2, setCount2] = useState(0); const increment2 = useCallback(() => setCount2(c => c + 1), []); return ( <> <CounterItem count={count1} label="左" onClick={increment1} /> <CounterItem count={count2} label="右" onClick={increment2} /> </> ); };うまくメモ化できていることが確認できる。
useMemo
useCallbackはコールバック関数をメモ化したが、useMemoは値をメモ化する。
useMemoのユースケースは2つある。useMemoのユースケース1
オブジェクトをコンポーネントのpropsとして渡す前に、オブジェクトをメモ化する
useMemoを使用しない場合
const CounterItem = React.memo(({ params }) => { console.log(`${params.label}がレンダリング`); return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>; }); const CounterList = () => { const [count1, setCount1] = useState(0); const increment2 = useCallback(() => setCount2(c => c + 1), []); const [count2, setCount2] = useState(0); const increment1 = useCallback(() => setCount1(c => c + 1), []); const params1 = { label: '左', value: count1, handler: increment1 } const params2 = { label: '右', value: count2, handler: increment2 } return ( <> <CounterItem params={params1} /> <CounterItem params={params2} /> </> ); };上記のコードの場合も片方のボタンを押すと両方のボタンが再レンダリングされており、うまくメモ化できていない。左ボタンを押した際にはparams1のcount1が変更されるので左ボタンは再レンダリングされるべきだが、右ボタンの再レンダリングはスキップしたい。
上記の問題も参照の同一性が関係している。オブジェクトの中身が同じでも同じ参照にはならない。
{'a': 1} === {'a': 1} // false [1,2,3] === [1,2,3] // falsestateの更新によりCounterListコンポーネントの再レンダリングが起きると、params1とparams2が再度定義される。つまりCounterItemコンポーネントに渡すparamsが同じ参照にはならないため、例えuseCallbackでコールバック関数をメモ化しても、最終的に渡されるpropsの参照が違うのでレンダリングがスキップされない。
useMemoを使用する場合
上記の問題をuseMemoを使用することで解決できる。
const CounterItem = React.memo(({ params }) => { console.log(`${params.label}がレンダリング`); return <button onClick={params.handler}><div>{params.label}: {params.value}</div></button>; }); const CounterList = () => { const [count1, setCount1] = useState(0); const increment2 = useCallback(() => setCount2(c => c + 1), []); const [count2, setCount2] = useState(0); const increment1 = useCallback(() => setCount1(c => c + 1), []); const params1 = useMemo(() => ({ label: '左', value: count1, handler: increment1 }), [count1, increment1]) const params2 = useMemo(() => ({ label: '右', value: count2, handler: increment2 }), [count2, increment2]) return ( <> <CounterItem params={params1} /> <CounterItem params={params2} /> </> ); };うまくメモ化できているのが確認できる。
useMemoのユースケース2
重い計算結果をメモ化する。
useMemoを使用しない場合
// 何かしらの重い計算 const heavyCalc = (count) => { let i = 0 while (i < 2000000000) i++ return count } const Counter = () => { const [count1, setCount1] = useState(0); const increment1 = () => setCount1(c => c + 1); const [count2, setCount2] = useState(0); const increment2 = () => setCount2(c => c + 1); const result = heavyCalc(count1) return ( <> <button onClick={increment1}><div>重い計算あり: {result}</div></button> <button onClick={increment2}><div>重い計算なし: {count2}</div></button> </> ) };どちらのボタンを押しても、コンポーネント内の重い計算処理のせいで再レンダリングが遅れる。重い計算があるheavyCalc関数に渡しているのはcount1だけなので、右ボタンを押した場合はすぐ再レンダリングして欲しい。
useMemoを使用する場合
上記の問題をuseMemoを使用することで解決できる。
// 何かしらの重い計算 const heavyCalc = (count) => { let i = 0 console.log('重い計算開始') while (i < 2000000000) i++ return count } const Counter = () => { const [count1, setCount1] = useState(0); const increment1 = () => setCount1(c => c + 1); const [count2, setCount2] = useState(0); const increment2 = () => setCount2(c => c + 1); const result = useMemo(() => heavyCalc(count1), [count1]) return ( <> <button onClick={increment1}><div>重い計算あり: {result}</div></button> <button onClick={increment2}><div>重い計算なし: {count2}</div></button> </> ) };左ボタンを押した時は重い計算処理が走るが、右ボタンを押した場合はメモ化した値を返すため無駄な重い計算処理は走っていないので再レンダリングが早い。