今日作成するアプリ
1.Reactの簡単な説明
- データの変更を検知したら、関連する部分だけを効率的に更新、描画する
- 仮想 DOM(インメモリに保持されたUI表現)による高速な描画
- JSXを使う(JavaScriptのソースコードにHTML的なものを埋め込む)
- 単一のWebページでアプリケーション(Single Page Application)を作れる
Reactの採用事例
主な採用事例
- Netflix
- Slack
- Uber
- Airbnb
- Paypal
ES2015(ES6)について
- ECMASCriptの6th Editionのこと
- letとconstで変数を宣言できる
- アロー関数 : console.log(materials.map(material => material.length));
- Class構文
- extendsでクラスの継承
- 全てのブラウザで対応しているわけではないため、Babelというトランスパイラを利用する
- ReactはES6かES7で書く場合が多い
2.新しいシングルページアプリケーションを作成する
create-react-appという新規のReactプロジェクトを作るCLIツールを使います。
まずは、create-react-appをインストールします。
(Node >= 8.10 及び npm >= 5.6 の環境が必要です)
npm install -g create-react-app
todoプロジェクトを作成します。
npx create-react-app todo
3.ディレクトリ構成についての説明
.
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html Reactアプリケーションがのるページ
├── src
│ ├── App.css App.jsで使用されるcss
│ ├── App.js index.jsから呼ばれるReactコンポーネント
│ ├── App.test.js
│ ├── index.css index.jsで使われるcss
│ ├── index.js Reactアプリケーションで最初に走るスクリプト(ルート DOM ノードにレンダリングする処理が書かれている)
│ └── logo.svg
└── yarn.lock
4.実行する
次のコマンドを実行します。
npm start
ブラウザに次のように表示されたら成功です。(http://localhost:3000のURLでブラウザが起動します)
5.ソースを読んでみる
src/index.jsを開きます
以下の処理はpublic/index.htmlの<div id="root"></div>
にAppコンポーネントをレンダリングしています。
ReactDOM.render(
<App />,
document.getElementById('root')
);
src/App.jsを開きます
AppクラスがReact.Componentを継承しReact コンポーネントになっています。React.Component サブクラスで必ず定義しなければならない唯一のメソッドは render() です。render() メソッドは変更が起こるたびに呼び出されます。
returnで返しているのはJSXです。classはclassNameと書かないといけないことに注意してください。
export default App
でこのファイルのデフォルトとしてAppをexportしています。
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
6.コンポーネントを作る
目的:コンポーネントの作り方と使い方
作成したプロジェクトの中に移動してください。
cd todo
1) コンポーネントを入れるためのディレクトリを作ります。
mkdir -p src/components
2) componentsディレクトリにList.jsを作ります。
List.js
import React, { Component } from 'react';
class List extends Component {
render() {
return (
<table>
<tbody>
<tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>掃除する</td></tr>
<tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>買い物</td></tr>
<tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>洗濯する</td></tr>
</tbody>
</table>
);
}
}
export default List;
3) App.jsに組み込み
import React, { Component } from 'react';
import List from './components/List';
class App extends Component {
render() {
return (
<List />
);
}
}
export default App;
7.stateを使う
目的:stateの使い方
stateはコンポーネントの内部で制御されるオブジェクトです。更新はsetStateにより非同期に更新されます。stateが更新されると再描画されます。
List.jsを次のように書き換えます。
import React, { Component } from 'react';
class List extends Component {
constructor(props) {
super(props);
this.state = {
todos: [
'掃除する',
'買い物',
'洗濯する'
]
};
}
onClickAdd() {
const newTodo = window.prompt("やることを入力してください", "");
const todos = this.state.todos;
todos.push(newTodo);
this.setState({todos});
}
render() {
return (
<div>
<div><button onClick={() => this.onClickAdd()}>追加</button></div>
<table>
<tbody>
{this.state.todos.map((todo) => (
<tr key={todo}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
))}
</tbody>
</table>
</div>
);
}
}
export default List;
追加でリストが増えると成功です。
8.propsを使う
目的:propsの使い方
propsはコンポーネントに属性として設定し値を渡すことができます。値だけでなく関数も渡すことができます。
1) componentsディレクトリにItem.jsを作ります。
import React, { Component } from 'react';
class Item extends Component {
render() {
const {todo, onClickItem} = this.props;
return (
<tr onClick={() => onClickItem(todo)}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
);
}
}
export default Item;
2) List.jsのrenderを次のように書き換えます。
Itemをimportしてください。
import Item from './Item';
render() {
return (
<div>
<div><button onClick={() => this.onClickAdd()}>追加</button></div>
<table>
<tbody>
{this.state.todos.map((todo) => (
<Item key={todo} todo={todo} onClickItem={(todo) => alert(todo)} />
))}
</tbody>
</table>
</div>
);
}
9.詳細内容表示用のコンポーネントの追加
1) src/componentsディレクトリにContent.jsを追加します。
import React, { Component } from 'react';
class Content extends Component {
render() {
return (
<div style={{marginLeft: 50}}>
<span>掃除をする</span>
</div>
);
}
}
export default Content;
2) App.jsを次のように書き換えます。
import React, { Component } from 'react';
import List from './components/List';
import Content from './components/Content';
class App extends Component {
render() {
return (
<div style={{display:'flex'}}>
<List />
<Content />
</div>
);
}
}
export default App;
Listコンポーネントで選択した内容をContentコンポーネントに表示させたいですが、このままではうまく行きません。
10.reduxとredux-sagaの導入
目的:redux・redux-sagaの導入方法と使い方
Reduxは、Reactのstate(状態)を管理をするためのフレームワークです。
redux-sagaとは
redux-saga は、アプリケーションの副作用(つまり、データフェッチのような非同期のものや、ブラウザキャッシュへのアクセスのような不純なも> > の)を管理しやすく、実行効率が高く、テストが簡単で、障害処理を改善することを目的としたライブラリです。
1) ライブラリをインストールします
npm install redux react-redux redux-saga
npm install
2) actionの作成
mkdir -p src/actions
src/actions/index.js
/**
* Redux Actions
*/
export * from './TodoAppActions';
src/actions/TodoAppActions.js
/**
* Todo App Actions
*/
import {
GET_TODOS,
GET_TODOS_SUCCESS,
GET_TODOS_FAILURE,
ADD_TODO,
ADD_TODO_SUCCESS,
SELECT_TODO,
SELECT_TODO_SUCCESS,
} from './types';
export const getTodos = () => ({
type: GET_TODOS
});
export const getTodosSuccess = (response) => ({
type: GET_TODOS_SUCCESS,
payload: response
});
export const getTodosFailure = (error) => ({
type: GET_TODOS_FAILURE,
payload: error
});
export const addTodo = (todo) => ({
type: ADD_TODO,
payload: todo
});
export const addTodoSuccess = (todo) => ({
type: ADD_TODO_SUCCESS,
payload: todo
});
export const selectTodo = (todo) => ({
type: SELECT_TODO,
payload: todo,
});
export const selectTodoSuccess = (todo) => ({
type: SELECT_TODO_SUCCESS,
payload: todo,
});
src/actions/types.js
export const GET_TODOS = 'GET_TODOS';
export const GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS';
export const GET_TODOS_FAILURE = 'GET_TODOS_FAILURE';
export const ADD_TODO = 'ADD_TODO';
export const ADD_TODO_SUCCESS = 'ADD_TODO_SUCCESS';
export const SELECT_TODO = 'SELECT_TODO';
export const SELECT_TODO_SUCCESS = 'SELECT_TODO_SUCCESS';
3) sagasの作成
mkdir -p src/sagas
src/sagas/index.js
/**
* Root Sagas
*/
import { all } from 'redux-saga/effects';
// sagas
import todoSagas from './Todo';
export default function* rootSaga(getState) {
yield all([
todoSagas(),
]);
}
src/sagas/Todo.js
import { all, call, fork, put, takeEvery } from 'redux-saga/effects';
import {
GET_TODOS,
SELECT_TODO,
ADD_TODO,
} from '../actions/types';
import {
getTodosSuccess,
getTodosFailure,
selectTodoSuccess,
addTodoSuccess,
} from '../actions';
const getTodosRequest = () => new Promise((resolve, reject) => {
const todos = ['部屋の掃除', '買い物', '洗濯'];
resolve(todos);
});
function* getTodosFromServer() {
try {
const response = yield call(getTodosRequest);
yield put(getTodosSuccess(response));
} catch (error) {
yield put(getTodosFailure(error));
}
}
function* addTodoToServer(action) {
yield put(addTodoSuccess(action.payload));
}
function* selectTodoFromServer(action) {
yield put(selectTodoSuccess(action.payload));
}
export function* getTodos() {
yield takeEvery(GET_TODOS, getTodosFromServer);
}
export function* selectTodo() {
yield takeEvery(SELECT_TODO, selectTodoFromServer);
}
export function* addTodo() {
yield takeEvery(ADD_TODO, addTodoToServer);
}
export default function* rootSaga() {
yield all([
fork(getTodos),
fork(selectTodo),
fork(addTodo),
]);
}
function*についてはこちら
yieldについてはこちら
takeEvery: Actionがdispatchされるたびに起動させたいタスクを指定します
put: Actionをdispatchします
4) reducersの作成
mkdir -p src/reducers
src/reducers/index.js
/**
* App Reducers
*/
import { combineReducers } from 'redux';
import todoAppReducer from './TodoAppReducer';
const reducers = combineReducers({
todoApp: todoAppReducer,
});
export default reducers;
src/reducers/TodoAppReducer.js
/**
* Todo App Reducer
*/
// action types
import {
GET_TODOS,
GET_TODOS_SUCCESS,
GET_TODOS_FAILURE,
ADD_TODO_SUCCESS,
SELECT_TODO_SUCCESS,
} from '../actions/types';
// initial state
const INIT_STATE = {
todos: [],
selectedTodo: '',
};
export default (state = INIT_STATE, action) => {
switch (action.type) {
case GET_TODOS:
return { ...state, todos: [] };
case GET_TODOS_SUCCESS:
return { ...state, todos: action.payload };
case GET_TODOS_FAILURE:
return {}
case ADD_TODO_SUCCESS:
const newTodos = [];
state.todos.forEach((todo) => newTodos.push(todo));
newTodos.push(action.payload);
return { ...state, todos: newTodos };
case SELECT_TODO_SUCCESS:
return { ...state, selectedTodo: action.payload };
default: return { ...state };
}
}
5) storeの追加
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import { Provider } from "react-redux";
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from "redux";
import reducers from './reducers';
import RootSaga from "./sagas";
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
reducers,
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(RootSaga);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
,
document.getElementById('root')
);
6) コンポーネントの修正
src/components/Content.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
class Content extends Component {
render() {
const {selectedTodo} = this.props;
return (
<div style={{marginLeft: 50}}>
<span>{selectedTodo}</span>
</div>
);
}
}
const mapStateToProps = ({ todoApp }) => {
const { selectedTodo } = todoApp;
return { selectedTodo };
}
export default connect(mapStateToProps, null)(Content);
src/components/Item.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { selectTodo } from '../actions';
class Item extends Component {
render() {
const {todo, selectTodo} = this.props;
return (
<tr onClick={() => selectTodo(todo)}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
);
}
}
export default connect(null, { selectTodo })(Item);
src/components/List.js
import React, { Component } from 'react';
import Item from './Item';
import { connect } from 'react-redux';
import { getTodos, addTodo } from '../actions';
class List extends Component {
componentDidMount() {
const { getTodos } = this.props;
getTodos();
}
onClickAdd() {
const { addTodo } = this.props;
const newTodo = window.prompt("やることを入力してください", "");
addTodo(newTodo);
}
render() {
const {todos} = this.props;
return (
<div>
<div><button onClick={() => this.onClickAdd()}>追加</button></div>
<table>
<tbody>
{todos.map((todo) => (
<Item key={todo} todo={todo} />
))}
</tbody>
</table>
</div>
);
}
}
const mapStateToProps = ({ todoApp }) => {
const { todos } = todoApp;
return { todos };
}
export default connect(mapStateToProps, { getTodos, addTodo })(List);
11.フォームを利用する
1) src/componentsディレクトリにAddItem.jsを追加します。
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';
class AddItem extends Component {
constructor(props) {
super(props);
this.state = { todo: '' };
}
handleChange(event) {
this.setState({ todo: event.target.value });
}
onClickAdd() {
const { addTodo } = this.props;
addTodo(this.state.todo);
}
render() {
return (
<div style={{display: 'flex'}}>
<div><input type="text" value={this.state.todo} onChange={(event) => this.handleChange(event)} /></div>
<div><button onClick={() => this.onClickAdd()}>追加</button></div>
</div>
);
}
}
export default connect(null, { addTodo })(AddItem);
2) List.jsでAddItemコンポーネントを使用するように修正する
import React, { Component } from 'react';
import Item from './Item';
import AddItem from './AddItem';
import { connect } from 'react-redux';
import { getTodos } from '../actions';
class List extends Component {
componentDidMount() {
const { getTodos } = this.props;
getTodos();
}
render() {
const {todos} = this.props;
return (
<div>
<div><AddItem /></div>
<table>
<tbody>
{todos.map((todo) => (
<Item key={todo} todo={todo} />
))}
</tbody>
</table>
</div>
);
}
}
const mapStateToProps = ({ todoApp }) => {
const { todos } = todoApp;
return { todos };
}
export default connect(mapStateToProps, { getTodos })(List);
12.関数コンポーネントを使用する
AddItemコンポーネントを次のように修正します。
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions';
function AddItem(props) {
const [todo, setTodo] = useState('');
const onClickAdd = () => {
const { addTodo } = props;
addTodo(todo);
}
return (
<div style={{display: 'flex'}}>
<div><input type="text" value={todo} onChange={(event) => setTodo(event.target.value)} /></div>
<div><button onClick={() => onClickAdd()}>追加</button></div>
</div>
);
}
export default connect(null, { addTodo })(AddItem);
これまでのソースコードはGitHubに上げています。
https://github.com/yuichi0301/todo