Reactは公式ドキュメントを確認してTodoアプリを作ったりしましたが、いざ開発するとなると何からして良いか分からなくなりました。知っていないとならない基礎知識が相当あって全体を把握していないと難しいという印象です。
Reactの書き方だけでなく、JavascriptのES6の機能も把握しておく必要があり、フック、他のライブラリを使うならその知識も必要になってきます。
アプリケーション開発をするにあたって素のReactだけで作るのは色々と問題に直面するので便利なライブラリを選定して使う必要があります。
TypeScriptを使って開発効率を上げるのが最近のトレンドなのでテンプレートにTypeScriptを使います。よってTypeScriptの知識も必要になります。
とにかくシステム開発を一通りするために書籍を参考にするのが近道だと思います。
「TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発」を一通り読んで自分が理解したことを紹介します。興味があれば書籍を購入することをおすすめします。
まずは小さなアプリケーションを自分で作成してみます。
コンポーネントを追加してみる
const Hello = () => {
const onClick = () => {
console.log('Hello')
}
const text = 'Hello, React'
return <div onClick={onClick}>{text}</div>
}
export default Hello
コンポーネントに引数を渡す
const Text = (props: { content: string }) => {
const { content } = props
return <p>{content}</p>
}
export default Text
//Helloを使う
import Hello from './components/Hello'
//Messeageを使う
import Message from './components/Message'
function App() {
return (
<div className='App'>
<Hello></Hello>
<Message content='こんにちは'></Message>
</header>
</div>
)
}
export default App
クラスコンポーネントと関数コンポーネントの2種類ありますがシンプルに記載できる関数コンポーネントが主流のようです。フック機能により関数コンポーネントでステートの管理ができるようになったことが大きいようです。props
キーワードで引数を宣言します。
props
は親から子にデータを一方通行で渡すので子から親にわたすことは出来ないという制限があります。この制限は不都合なようで、バグを減らすために重要な意味を持つのです。一方通行が保証されているからそちらのルートだけ調べて逆走は無視して良いということになります。
引き渡されたprops
は変更ができません。const
で宣言しているので当たり前ですがconst
で受けなくても変更出来ない定数です。変更ができない制限はまたそのことが保証されているということです。
変更したい場合はコールバック関数を親からprops
に渡して子で実行することで行なえます。変更箇所はコールバック関数のみなので親のコンポーネントでどのように変更されるか把握できるというわけです。
直接スタイルを定義する
JSXではHTML要素で使われている属性は予約後なので使えません。onclickはonClick、classはclassName、CSSもbackground-colorはbackgroundColorのようにキャメルケースで表現します。
const Text = (props: { content: string }) => {
const { content } = props
return <p style={{ padding: '16px', backgroundColor: 'grey' }}>{content}</p>
}
export default Text
ただしスタイルを直接記述すると汎用性が損なわれるのでコンポーネントで設定をまとめてしまうのが良いようです。
複数メンバーで相談して決めていないと、クラス名前がバラバラになってしまったり、フォントの大きさが画面ごとにばらばらになったりすることはよくあります。
他の要素を引数に渡す
属性だけでなく他の要素も引数に渡すことが出来ます。<p>
要素を引数として渡すサンプルです。
const Container = (props: { title: string; children: React.ReactElement }) => {
const { title, children } = props
return (
<div style={{ background: 'red' }}>
<span>{title}</span>
<div style={{ background: 'blue' }}>{children}</div>
</div>
)
}
const Parent = () => {
return (
<Container title='Hello'>
<p>ここが子供要素</p>
</Container>
)
}
export default Parent
Reactの型
コンポーネントが受け取る引数の型を定義するのが基本です。型を定義することでその後コンポーネントを実装時に定義していた型と異なる実装をすると即座にチェックしてくれるようになります。
type ContainerProps = {
title: string
children: React.ReactNode
}
const Container = (props: ContainerProps) => {
const { title, children } = props
return (
<div style={{ background: 'red' }}>
<span>{title}</span>
<div style={{ background: 'blue' }}>{children}</div>
</div>
)
}
const Parent = () => {
return (
<Container title='Hello'>
<p>ここが子供要素</p>
</Container>
)
}
export default Parent
例えば、上記の<Contaier>
でtitle
をミススペルするとエラーになります。
Contextを使ったコンポーネント間のデータの渡し方
Reactは親から子にprops
という引数でデータを一方向に渡すのが基本です。シンプルなルールのため弊害もあります。親->子->孫というコンポーネント構成で親から孫にだけ渡したいデータがある場合、間の子にもデータを渡す必要があります。子は自分では使わない引数を受け取って孫に渡します。もっと高階層になればなるほど面倒になると思いませんか。親から孫にデータを直接渡すことは原則出来ないのです。
解決方法の1つにContextがあります。
import React from 'react'
const TitleContext = React.createContext('') //(1)
//孫
const Title = () => {
return (
<TitleContext.Consumer> //(2)
{(title) => {
return <h1>{title}</h1>
}}
</TitleContext.Consumer>
)
}
//子
const Header = () => {
return <Title></Title>
}
//親
const Page = () => {
const title = 'Hello World'
return (
<TitleContext.Provider value={title}> // (3)
<Header></Header>
</TitleContext.Provider>
)
}
export default Page
- Contextを作成
- Consumerで子要素として関数を指定
- Providerでvalueにデータを渡す
上記の記述で子である<Header>
コンポーネントは引数を指定しなくとも、孫である <Title>
コンポーネントで引数を受け取れます。
後述するフックの機能(useContext)によりもっとシンプルに記述する方法もあります。
Hooks(フック)
フックは関数コンポーネントの内部で状態を管理する事ができます。これによりクラスコンポーネントでないと内部の状態を管理出来なかったが関数コンポーネントでも可能になり、関数コンポーネントですべて書くことが出来るようになった理解です。
useState
内部状態を管理するフックを使ってみます。
const [状態, 更新関数] = useState(初期状態)
useState
は返り値に状態と更新関数の配列を返します。更新関数を使って状態を更新します。この更新関数以外で状態を更新することは出来ません。
import { useState } from 'react'
type CounterProps = {
initialValue: number
}
const Counter = (props: CounterProps) => {
const { initialValue } = props
const [count, setCount] = useState(initialValue)
return (
<div>
<p>Count:{count}</p>
<button
onClick={() => {
setCount(count + 1)
}}>
+(値版)
</button>
<button
onClick={() => {
setCount(count - 1)
}}>
-
</button>
<button
onClick={() => {
setCount((prevCount) => prevCount + 1)
}}>
+(関数版)
</button>
</div>
)
}
export default Counter
直感的には状態を使って更新関数を実行するのが私は分かりやすいです(値版)。公式のリファレンスは関数型の更新で説明されています(関数版)。引数は前の状態を参照するようです。変数名は揃っていれば何でも良いようです。
useReducer
useReducer
は複雑な状態をシンプルに記述することが出来ます。
- 更新関数(dispatch)にactionと呼ばれるデータを渡す
- reducer関数は、現在の状態とactionを渡すと次の状態を返す
reducer(現在の状態, action){
return 次の状態
}
const [現在の状態, dispatch] = useReducer(reducer, 初期状態)
import { useReducer } from 'react'
type Action = 'DECREMENT' | 'INCREMENT' | 'DOUBLE' | 'RESET'
const reducer = (currentCount: number, action: Action) => {
switch (action) {
case 'DECREMENT':
return currentCount - 1
case 'INCREMENT':
return currentCount + 1
case 'DOUBLE':
return currentCount * 2
case 'RESET':
return 0
}
}
type CounterProps = {
initialValue: number
}
const Counter = (props: CounterProps) => {
const { initialValue } = props
const [count, dispatch] = useReducer(reducer, initialValue)
return (
<div>
<p>Count: {count} </p>
<button onClick={() => dispatch('DECREMENT')}>-</button>
<button onClick={() => dispatch('INCREMENT')}>+</button>
<button onClick={() => dispatch('DOUBLE')}>x2</button>
<button onClick={() => dispatch('RESET')}>Reset</button>
</div>
)
}
export default Counter
これで dispatch
と action
により状態を更新をする処理がスッキリするようになりました。reducer
関数を見ればどのような action
で状態がどのようになるかも見通しが良くなります。
メモ化
Reactは下記の条件で再描画を行います。
- propsや内部状態の更新
- コンポーネント内で参照しているContextの更新
- 親コンポーネントが再描画
親コンポーネントが再描画されると子コンポーネントも再描画されます。
この再描画を制御する機能のことをメモ化と呼ぶようです。
これによりアプリケーションのパフォーマンスが向上します。
メモ化を使うと親コンポーネントの再描画で関係ない子コンポーネントの再描画を制御出来ます。
使い方は関数コンポーネントをmemo関数でラップするだけです。
import React, { memo, useState } from 'react'
type FizzProps = {
isFizz: boolean
}
const Fizz = (props: FizzProps) => {
const { isFizz } = props
console.log(`Fizzが再描画されました。isFizz=${isFizz}`)
return <span>{isFizz ? 'Fizz' : ''}</span>
}
type BazzProps = {
isBazz: boolean
}
const Bazz = memo<BazzProps>((props) => {
const { isBazz } = props
console.log(`Bazzが再描画されました。isFizz=${isBazz}`)
return <span>{isBazz ? 'Bazz' : ''}</span>
})
const Parent = () => {
const [count, setCount] = useState(1)
const isFizz = count % 3 === 0
const isBazz = count % 5 === 0
console.log(`Parentが再描画されました。count=${count}`)
return (
<div>
<button onClick={() => setCount((c) => c + 1)}>+1</button>
<p> {`現在のカウント:${count}`} </p>
<p>
<Fizz isFizz={isFizz}></Fizz>
<Bazz isBazz={isBazz}></Bazz>
</p>
</div>
)
}
export default Parent
Parent
の count
を更新すると、Parent
と Fizz
は再描画されますが Bazz
は isBazz
が変わらなければ再描画されません。サンプルコードを動かしてログを確認することでその状況が確認出来ます。
useCallback
先程のメモ化したコンポーネントに関数を渡すようにします。
ソースは冗長なのでgitHubの差分を参照してください。
親の再描画で引数に渡す関数が更新されるためメモ化したコンポーネントであっても更新ありとなって再描画されてしまうようになります。
コンポーネントではなく関数をメモ化するために useCallback
を使います。これでコンポーネントの再描画を制御することが出来ました。
ソースは冗長なのでgitHubの差分を参照してください。
useEffect
関数の実行をコンポーネント描画後に実行する事ができます。useEffect
がないと状態の更新と再描画を繰り返す無限ループに陥る可能性があります。
このような描画に関係がない処理のことを副作用と呼ぶようです。
- DOMを手動で変更する
- ログを出力する
- データの取得
などが副作用の処理としてあげられます。データの取得は非同期でサーバーからデータを取得するのは定番なのでよく使われるフックだと思います。
import { useState, useEffect } from 'react'
const Title = () => {
const [count, setCount] = useState(0)
useEffect(() => {
document.title = `Youre clicked ${count} times`
}, [count])
return (
<div>
<p> ${count} 回クリックしました。 </p>
<button
onClick={() => {
setCount(count + 1)
}}>
クリック
</button>
</div>
)
}
export default Title
useEffect
の第二引数は依存配列で、そこに指定した変数が更新されると関数が実行されます。何も指定しないと初期のコンポーネント描画に一度だけ実行されます。
useContext
Contextから値を参照するフックです。
「Contextを使ったコンポーネント間のデータの渡し方」で useContext
を使うともっとスッキリかけます。こちらのほうが直感的にかけるように思います。
ソースは冗長なのでgitHubの差分を参照してください。
refのフック
useState
や useReducer
は関数コンポーネントで値を保持しますが状態を更新するたびにコンポーネントが再描画されます。 useRef
は2つ機能があり、更新しても再描画しないようにすることと、DOMの参照です。
import { useRef, useState } from 'react'
const RefSample = () => {
const num = useRef(100)
const inputEL = useRef<HTMLInputElement | null>(null)
const [text, setText] = useState('')
const increment = () => {
num.current += 1
}
const handleClick = () => {
if (inputEL.current !== null) {
setText(inputEL.current.value)
}
}
return (
<div>
<h2>値参照</h2>
<p onClick={increment}>num : {num.current} </p>
<h2>DOM参照</h2>
<input
ref={inputEL}
type='text'></input>
<button onClick={handleClick}>テキスト表示</button>
<p>入力した値は: {text} </p>
</div>
)
}
export default RefSample
increment
関数をいくら実行してもコンポーネントは描画されないので画面に表示されません。
DOM参照で input
を参照し、入力文字列を handleClick
関数でinput要素のvalue値を参照してコンポーネントを再描画しています。合わせて num
変数もクリックした分増加した値が表示されます。
入力文字列が増減するたびにコンポーネントを再描画するのは効率が悪いので useRef
でDOM要素を参照しています。値の更新で再描画が必要ない場合に使うとパフォーマンスが上がります。
コンポーネント設計に続く