2021/07/25
React
## 環境構築
1. Node.js及びnpmまたはyarnをインストール
1. React開発環境を構築する「create-react-app」をインストール
```
npm install -g create-react-app
```
※ -g ...グローバルインストール
1. Reactプロジェクトを作成
```
create-react-app <app-name>
```
## 基本
### コンポーネントについて
- クラスコンポーネントと関数コンポーネントがある
- コンポーネント名はアッパーキャメルケース
### JSXについて
- ルートノードは1つ
- ルートノードを出力したくない場合は`React.Fragment`をルートノードにする (`<></>` でもOK)
- 通常のfor文はエラーになるため、即時関数をアローにする
- html属性は下記のように使用
- class: `className="btn btn-success"`
- for: `htmlFor="password"`
- style: `style={{color: "red"}}`
### propsについて
- 親コンポーネントから渡されるプロパティ
- 変更不可
- デフォルト値の設定可
- 検証可
### stateについて
- コンポーネントによって保持される状態
- クラスコンポーネントで使用可
- 変更可
- privateであるべき
### サンプル
`src/index.js`
```js
import React, { Component, useState } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from "prop-types";
// クラスコンポーネント
class App extends Component {
ANIMALS = [{name:"cat", bark:"meow!"}, {name:"dogs", bark:"bow!"}]
constructor(props) {
// 親クラスのコンストラクター呼び出し
super(props)
// props = 親コンポーネントから渡されたプロパティ(不変)
let stateMsg = props.msg.replace('props', 'state')
// state = Appクラスが持つ状態オブジェクト(可変)
this.state = { msg: stateMsg, color: "red", animals: this.ANIMALS}
// 関数をバインド(バインドしないとthisが使えない)
this.changeMsg = this.changeMsg.bind(this)
}
changeMsg() {
// stateは直接変えると、変数は変わるがviewが更新されない(warningも発生)
console.log("更新前 state.msg: "+this.state.msg)
this.state.msg += ":[更新]"
console.log("更新後 state.msg: "+this.state.msg)
}
changeColor = color => {
// setStateを使うとviewが更新される(setStateは渡されたデータと元々のstateをマージして更新してくれる)
this.setState({color: this.state.color === color ? "red" : color})
}
// アロー関数は宣言時のthisで束縛されるので、下記はバインドしなくてもAppクラスのthisが使える
addCow = () => {
// setStateは関数も渡せる
this.setState((state) => ({ animals: state.animals.push({name:"cow", bark:"moo!"})}))
}
render() {
return (
<div> {/* JSXのルートノードは1つにしないとエラーになる */}
<p>props.msg: <strong style={{color: this.state.color}}>{this.props.msg}</strong></p>
<p>state.msg: <strong style={{color: this.state.color}}>{this.state.msg}</strong></p>
<button onClick={this.changeMsg}>メッセージ変更</button>
{/* JSXでイベントハンドラに引数は渡せないため、bindで渡す */}
<button onClick={this.changeColor.bind(this, "blue")}>色変更</button>
<div>
<hr />
<h2>Animalコンポーネント</h2>
{/* ループ処理時はkey属性をつけないとwarningが出る */}
{this.ANIMALS.map((animal, index) => (
<div key={index}><Animal name={animal.name} bark={animal.bark} firstCount={2} /></div>
))}
<div><Animal bark="baa!" /></div>
<button onClick={this.addCow}>牛を追加</button>
</div>
</div>
)
}
}
// 関数コンポーネント
function Animal(props) {
// useState = 関数コンポーネントでsetStateを使用するためのフック(return [0:初期値、1:setState関数])
const [bark, setBark] = useState(props.bark)
const [barks, setBarks] = useState([])
function changeBark() {
setBark(bark+":[更新]")
}
function addBark() {
// setStateはstateに変更があった場合(Object.is()で判定)に再描画される
// pushやspliceでは変更なしと判定されて再描画されないため、新しいオブジェクトを生成して渡す必要がある
setBarks([...barks, bark]) // ... = スプレッド構文(配列を展開)
}
return (
<React.Fragment> {/* React.Fragmentを使うとルートノードが出力されない */}
{props.name}:
{/* JSXでforを使う場合、即時関数をアローにするのが良い */}
{(() => {
const items = []
for (let i=0; i<props.firstCount; i++) items.push(<strong style={{color: 'green'}} key={i}>{bark}</strong>)
return items;
})()}
{ barks.map((bark, index) => <strong key={index}>{bark}</strong>) }
<button onClick={addBark}>もっと鳴く</button>
<button onClick={changeBark}>鳴き声を変える</button>
</React.Fragment>
)
}
Animal.defaultProps = {
name: "default",
firstCount: 3
}
Animal.propTypes = {
name: PropTypes.string,
bark: PropTypes.string.isRequired,
firstCount: PropTypes.number
}
ReactDOM.render(<App msg="Appコンポーネントのprops.msg" />, document.getElementById('root'));
```
## Hooks
*参照: https://github.com/DiveIntoHacking/react-hooks-101*
- ver.16.8 から追加された機能
- state などの React の機能を、クラスを書かずに使える
- useState, useEffect
```js
import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
const App = (props) => {
const [name, setName] = useState(props.name)
const [price, setPrice] = useState(props.price)
// レンダリング時、priceまたはname変更時に呼ばれる
useEffect(() => {
console.log('useEffect1')
})
// レンダリング時に呼ばれる
useEffect(() => {
console.log('useEffect2')
}, [])
// レンダリング時、nameが変更されたときに呼ばれる
useEffect(() => {
console.log('useEffect3')
}, [name])
return (
<>
<p>{name}は{price}円です。</p>
<button onClick={()=>setPrice(price+1)}>+1</button>
<button onClick={()=>setPrice(price-1)}>-1</button>
<button onClick={()=>{setName(props.name); setPrice(props.price)}}>Reset</button>
<input value={name} onChange={e => setName(e.target.value)} />
</>
)
}
App.defaultProps = {
name: 'いくら',
price: 1000
}
ReactDOM.render(<App />, document.getElementById('root'))
```
- useReducer
```js
import React, { useState, useReducer } from 'react'
// import 'bootstarp' // 全てインポートする場合、jqueryがないとエラーになる
import 'bootstrap/dist/css/bootstrap.min.css'
import reducer from '../reducers'
const App = () => {
// useReducer(reducer, 初期状態 [, 初期化時のコールバック関数])
const [state, dispatch] = useReducer(reducer, [])
const [title, setTitle] = useState('')
const [body, setBody] = useState('')
const addEvent = () => {
dispatch({ type: 'CREATE_EVENT', title, body })
setTitle('')
setBody('')
}
const deleteAllEvents = () => {
if (window.confirm('全てのイベントを削除してよろしいですか?'))
dispatch({ type: 'DELETE_ALL_EVENT' })
}
return (
<div className="container my-3">
<h4>イベント作成フォーム</h4>
<form>
<div className="form-group">
<label htmlFor="title">タイトル</label>
<input name="title" id="title" className="form-control" value={title} onChange={e => setTitle(e.target.value)} />
</div>
<div className="form-group">
<label htmlFor="body">ボディ</label>
<textarea name="body" id="body" className="form-control" value={body} onChange={e => setBody(e.target.value)} />
</div>
<div className="mt-3">
<button className="btn btn-primary" type="button" onClick={addEvent} disabled={title === '' || body === ''}>イベントを作成する</button>
<button className="btn btn-danger" type="button" onClick={deleteAllEvents} disabled={state.length === 0}>全てのイベントを作成する</button>
</div>
</form>
<hr />
<h4>イベント一覧</h4>
<table className="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>タイトル</th>
<th>ボディ</th>
<th></th>
</tr>
</thead>
<tbody>
{state.map((event, index) => {
const handleClickDeleteButton = () => {
if (window.confirm(`イベント(id=${event.id})を削除してよろしいですか?`))
dispatch({ type: 'DELETE_EVENT', id: event.id })
}
return (
<tr key={index}>
<td>{event.id}</td>
<td>{event.title}</td>
<td>{event.body}</td>
<td><button type="button" className="btn btn-sm btn-danger" onClick={handleClickDeleteButton}>削除</button></td>
</tr>
)
})}
</tbody>
</table>
</div>
)
}
export default App
```
- useContext
- `src/contexts/AppContext.js`
```js
import { createContext } from 'react'
const AppContext = createContext()
export default AppContext
```
- `src/components/App.js`
```js
import AppContext from '../contexts/AppContext.js'
...
return (
<AppContext.Provider value={{ state, dispatch }}>
<EventForm />
</AppContext.Provider>
)
```
- `src/components/EventForm.js`
```js
import import React, { useContext } from "react"
AppContext from "../contexts/AppContext"
...
const { state, dispatch } = useContext(AppContext)
```
## パッケージ
- redux: コンポーネント間のstate共有
- react-redux: 同上
- react-route-dom: ルーディング
- redux-form: 入力フォーム操作
- redux-devtools-extension: デバッグ
- axios: httpリクエスト
- react-thunk: 非同期処理
- lodash: 配列操作(標準パッケージ)
```js
import _ from 'lodash'
console.log(_.mapKeys(action.response.data, 'id')) // idをキーにした配列に変換
```
## Redux
Redux ... コンポーネント間のstateの共有を実現
- combineReducers(複数のreducerを1つに統合する)
- `src/reducers/index.js`
```js
import { combineReducers } from "redux"
import events from "./events"
import operationLogs from "./operationLogs"
export default combineReducers({ events, operationLogs })
```
- `src/components/App.js`
```js
const initialState = {
events: [],
operationLogs: []
}
const [state, dispatch] = useReducer(reducer, initialState)
```