Reactの勉強
#React #webpack
勉強用Githubリポジトリ
まとめ
JSXをincludeしてコンポーネント分けする。
状態管理などは普通にJSのexport defaultで読み込む
thisの内容が結構変わるので、なんかうまくいかないときはbindを使う
やっぱりVueの方が好き
むしろWebpackの勉強になった
React の基本
今から始めるReact入門 〜 React の基本 - Qiita
webpack環境とreactの導入
$ mkdir react-tutorial
$ cd react-tutorial
$ mkdir -p src/js
$ npm init
entry point: (index.js) webpack.config.js
$ npm install --save-dev webpack webpack-cli webpack-dev-server
$ npm install -g webpack webpack-cli
$ npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
$ npm install --save-dev react react-dom
code: webpack.config.js
var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');
var path = require('path');
module.exports = {
context: path.join(__dirname, "src"),
entry: "./js/client.js",
module: {
rules: [{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
use: [{
loader: 'babel-loader',
options: {
presets: '@babel/preset-react', '@babel/preset-env'
}
}]
}]
},
output: {
path: __dirname + "/src/",
filename: "client.min.js",
publicPath: "/"
},
devServer: {
contentBase: "./src",
hot: true
},
plugins: debug ? [] : [
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
]
};
node.js - Webpack-dev-server serves a directory list instead of the app page - Stack Overflow
code: src/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React Tutorials</title>
<!-- change this up! http://www.bootstrapcdn.com/bootswatch/ -->
<!-- これ読み込むとなんかかっこいいフォントになる! -->
<link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/cosmo/bootstrap.min.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<div id="app"></div>
<script src="client.min.js"></script>
</body>
</html>
code: src/js/client.js
import React from "react";
import ReactDOM from "react-dom";
class Layout extends React.Component {
render() {
return (
<h1>Welcome!</h1>
);
}
}
const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);
$ webpack --mode development
$ open -a '/Applications/Google Chrome.app' src/index.html
Welcome!
code: src/js/client.js
- <h1>Welcome!</h1>
+ <h1>It works!</h1>
$ webpack --mode development
It works!
$ webpack serve --mode development
modeつけないと5s(production build)、modeつけると83ms
http://localhost:8080
It works!
client.jsなど、JSXを書くファイルは.jsxという拡張子にした方がシンタックスハイライトが効きやすそう
その場合はimportとwebpack.config.jsのjsのところをjsxに適宜直す
JSX
2つのtagを並べてreturnできない
{}で式評価
code: src/js/client.js
class Layout extends React.Component {
constructor() {
super();
this.hobby = 'guitar';
}
render() {
let name = "Tsutomu";
return (
<div>
<h1>It's {name}!</h1>
<h1>I'll be {this.get_result(20)}!</h1>
<h1>I like {this.hobby}!</h1>
</div>
);
}
get_result(num) {
return 1 + num;
}
}
Reactのコンポーネント化
code: src/js/components/Layout.js
import React from "react";
import Header from "./Header";
import Footer from "./Footer";
export default class Layout extends React.Component {
render() {
// keyがないとWarningが出る
let components = <Header key='header'/>, <Footer key='footer'/>;
return (
<div>
{ components }
</div>
);
}
}
code: src/js/components/Header.js
import React from "react";
import Title from "./Header/Title";
export default class Header extends React.Component {
render() {
return (
<header>
<Title />
</header>
);
}
}
code: src/js/components/Title.js
import React from "react";
export default class Title extends React.Component {
render() {
return (
<h1>Welcome!</h1>
);
}
}
code: src/js/components/Footer.js
import React from "react";
export default class Footer extends React.Component {
render() {
return (
<footer>Footer</footer>
);
}
}
code: src/js/client.js
import React from "react";
import ReactDOM from "react-dom";
import Layout from "./components/Layout"
const app = document.getElementById('app');
ReactDOM.render(<Layout/>, app);
React のstate とライフサイクル
React.Componentクラスは状態変数stateを持っており、これで状態管理ができる
setStateで状態を変化させ、状態が変化すると対応するDOMだけが変化する
code: src/js/components/Layout.js
export default class Layout extends React.Component {
constructor() {
super();
this.state = {name: "Tsutomu"};
}
render() {
setTimeout(() => {
this.setState({ name: 'Hello' })
}, 1000)
return (
<div>
<Header />
{ this.state.name }
<Footer />
</div>
);
}
}
Reactのprops
propsでコンポーネントに渡された引数を参照できる
code: src/js/components/Layout.js
export default class Layout extends React.Component {
constructor() {
super();
this.state = {title: "Welcome!", name: "Tsutomu"};
}
render() {
setTimeout(() => {
this.setState({ title: "Nice to meet you!", name: "Hello" })
}, 1000)
return (
<div>
<Header title={this.state.title}/>
{ this.state.name }
<Footer />
</div>
);
}
}
code: src/js/components/Header.js
export default class Header extends React.Component {
render() {
return (
<header>
<Title title={this.props.title}/>
</header>
);
}
}
code: src/js/components/Header/Title.js
export default class Title extends React.Component {
render() {
return (
<h1>{this.props.title}</h1>
);
}
}
Eventとデータ変更
イベントハンドラ関数もpropsで受け渡しする
普通にイベントハンドラ関数を定義すると、その関数の中のthisは関数が定義された所になる。
thisをちゃんとLayoutやHeaderとし、stateやpropsへ関数内でアクセスするには、bind(this)を関数につける
code: src/js/components/Header.js
export default class Header extends React.Component {
handleChange(e) {
const title = e.target.value;
this.props.changeTitle(title);
}
render() {
return (
<header>
<Title title={this.props.title}/>
<input value={this.props.title} onChange={this.handleChange.bind(this)}/>
</header>
);
}
}
code: src/js/components/Layout.js
export default class Layout extends React.Component {
constructor() {
super();
this.state = {title: "Welcome!"};
}
changeTitle(title) {
this.setState({title});
}
render() {
return (
<div>
<Header title={this.state.title} changeTitle={this.changeTitle.bind(this)}/>
<Footer />
</div>
);
}
}
public class fields syntax () => {} で関数を定義すれば、bindを省略できる。
@babel/plugin-proposal-class-propertiesが必要
code: webpack.config.js
......
use: [{
loader: 'babel-loader',
options: {
presets: '@babel/preset-react', '@babel/preset-env',
plugins: [
'@babel/plugin-proposal-class-properties', { 'loose': true }
]
}
......
$ webpack serve --mode development
code: src/js/components/Header.js
export default class Header extends React.Component {
handleChange = (e) => {
const title = e.target.value;
this.props.changeTitle(title);
}
render() {
return (
<header>
<Title title={this.props.title}/>
<input value={this.props.title} onChange={this.handleChange)}/>
</header>
);
}
}
code: src/js/components/Layout.js
export default class Layout extends React.Component {
constructor() {
super();
this.state = {title: "Welcome!"};
}
changeTitle = (title) => {
this.setState({title});
}
render() {
return (
<div>
<Header title={this.state.title} changeTitle={this.changeTitle)}/>
<Footer />
</div>
);
}
}
React Router編
今から始めるReact入門 〜 React Router - Qiita
$ npm install --save-dev react-router react-router-dom
code: webpack.config.js
devServer: {
...
historyApiFallback: true # 追加
},
https://qiita.com/TsutomuNakamura/items/34a7339a05bb5fd697f2#react-router-の導入 + pages配下までコピペ
code: src/js/client.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route } from "react-router-dom";
import Layout from "./pages/Layout";
import Featured from "./pages/Featured";
import Archives from "./pages/Archives";
import Settings from "./pages/Settings";
const app = document.getElementById('app');
ReactDOM.render(
<Router>
<Layout>
<Route exact path="/" component={Featured}></Route>
<Route path="/archives" component={Archives}></Route>
<Route path="/settings" component={Settings}></Route>
</Layout>
</Router>,
app);
exactがあると/hogeや/fugaを無効にし、/またはのみを受け付ける
exactがないと/archivesだけでなく/archives/mypost、/archives/20201206などを受け付ける
code: src/js/pages/Layout.js
import React from "react";
import { Link } from "react-router-dom";
export default class Layout extends React.Component {
render() {
return (
<div>
<h1>KillerNews.net</h1>
// Layoutコンポーネントに渡されたRouteコンポーネントで今表示されているものを参照
{this.props.children}
<Link to="/archives">archives</Link>,
<Link to="/settings">settings</Link>
</div>
);
}
}
featuresで始まり、archivesとsettingsのページが入れ替わる
Bootstrapのclassでボタンを装飾したいが、classはJSの予約語なので通常はclassNameを使う
それは不便なのでbabel-plugin-react-html-attrsを導入する
$ npm install --save-dev babel-plugin-react-html-attrs
code: webpack.config.js
module: {
rules: [{
use: [{
options: {
plugins: [
'react-html-attrs' // 追加
code: src/js/pages/Layout.js
<Link to="/archives" class="btn btn-danger">archives</Link>
history apiを子コンポーネントでも使えるようにするにはwithRouterを使用する。
react-router-domのwithRouter apiを使う場面 - code-log
code: src/js/pages/Layout.js
import React from "react";
import { Link, withRouter } from "react-router-dom";
class Layout extends React.Component {
navigate = () => {
this.props.history.push("/")
}
render() {
return (
<div>
<h1>KillerNews.net</h1>
{this.props.children}
<Link to="/archives" class="btn btn-danger">archives</Link>
<Link to="/settings" class="btn btn-success">settings</Link>
<button class="btn btn-info" onClick={this.navigate}>featured</button>
</div>
);
}
}
export default withRouter(Layout);
this.props.history.pushブラウザで戻れる / this.props.history.replaceブラウザで戻れない
URLパラメータは/archives/:articleのように指定し、this.props.match.params.articleと受け取る
code: src/js/client.js
<Route exact path="/archives" component={Archives}></Route>
<Route path="/archives/:article" component={Archives}></Route>
code: src/js/pages/Layout.js
<Link to="/archives/some-other-articles" class="btn btn-warning">archives (some other articles)</Link>
code: src/js/pages/Archives.js
<h1>Archives ({this.props.match.params.article})</h1>
URLパラメータを正規表現で制限できる。(settings/extraaaaとかは404になる)
code: src/js/client.js
<Route path="/settings/:mode(main|extra)" component={Settings}></Route>
code: src/js/pages/Layout.js
<Link to="/settings/main" class="btn btn-success">settings</Link>
<Link to="/settings/extra" class="btn btn-success">settings (extra)</Link>
code: src/js/pages/Settings.js
const type = (this.props.match.params.mode == "extra"? " (for experts)": "");
return (<h1>Settings{type}</h1>);
クエリストリングをURLSearchParamsを使って取得する方法
code: src/js/pages/Layout.js
<Link to="/archives/some-other-articles?date=yesterday&filter=none" class="btn btn-warning">
archives (some other articles)
</Link>
<Link to="/archives?date=today&filter=hot" class="btn btn-danger">archives</Link>
code: src/js/pages/Archives.js
const query = new URLSearchParams(this.props.location.search)
let message = (this.props.match.params.article ? this.props.match.params.article + ", ": "")
+ "date=" + query.get("date") + ", filter=" + query.get("filter");
return (<h1>Archives ({message})</h1>);
URLSearchParamsをサポートしていなければquery-stringライブラリというのを使う
NavLinkを使うとそのリンクがアクティブの時につけるクラスをactiveClassNameプロパティで指定できる
code: src/js/pages/Layouts.js
<NavLink to="/settings/main" class="btn btn-success" activeClassName="btn-danger">settings</NavLink>
しかし、NavLink のto にquery string が含まれる場合、activeClassName に指定したclass が正しく反映されない
React Router v4 からの主な移行・変更点一覧 - Qiitaはここでの解説内容を全て含む
flux編
今から始めるReact入門 〜 flux編 - Qiita
ReactはViewレイヤのフレームワークであり、状態の保管機能は備わっていない。
fluxはSPAを実現するための状態管理デザインパターンで、Reactとは直接関係ない概念。
DBへのアクセスやデータの受け渡しをViewから分離することで見通しの良いJSXを実現できる。
https://gyazo.com/ca3062788d33bea81b63a0e60d31732f
React and Flux
Action: 何を(data)どう(type)変化させるかをまとめたもの
Dispatcher: Actionを受け取りDBやAPIにアクセスしたりするロジックを持つシングルトン
Store: Dispatcher が処理した結果を蓄え、View がレンダリングするためのデータを格納する複数のシングルトン
View: Storesのデータを検知し、そのデータをレンダリングして表示する。新たなActionを発火させるものもある。
実装について
my-react-js-tutorials/3-flux/00_start_point/todolist at master · TsutomuNakamura/my-react-js-tutorialsをコピペ
$ npm install --save-dev flux
code: src/js/dispatcher.js
import { Dispatcher } from "flux";
export default new Dispatcher;
code: src/js/stores/TodoStore.js
import { EventEmitter } from "events";
class TodoStore extends EventEmitter {
constructor() {
super();
this.todos = [
{
id: 113464613,
text: "Go Shopping",
complete: false
},
{
id: 235684679,
text: "Pay Water Bills",
complete: false
}
];
}
createTodo(text) {
const id = Date.now();
this.todos.push({
id, text, complete: false
});
this.emit("change");
}
getAll() {
return this.todos;
}
handleActions(action) {
switch(action.type) {
case "CREATE_TODO": {
this.createTodo(action.text);
}
}
}
}
const todoStore = new TodoStore;
dispatcher.register(todoStore.handleActions.bind(todoStore));
// windowを使うとdebug用にコンソールからアクセスできる
// window.todoStore = todoStore;
// window.dispatcher = dispatcher;
export default todoStore;
code: src/js/actions/TodoActions.js
import dispatcher from "../dispatcher";
export function createTodo(text) {
dispatcher.dispatch({
type: "CREATE_TODO",
text
});
}
code: src/js/pages/Todos.js
import React from "react";
import Todo from "../components/Todo";
import * as TodoActions from "../actions/TodoActions";
import todoStore from "../stores/TodoStore";
export default class Todos extends React.Component {
constructor() {
super();
this.state = {
todos: todoStore.getAll()
};
}
componentDidMount() {
todoStore.on("change", this.getTodos)
}
// 他のページに移動する時、ハンドラは自動で削除されないためUnmountのときに明示的に解除する必要がある
componentWillUnmount() {
todoStore.removeListener("change", this.getTodos);
}
getTodos = () => {
this.setState({
todos: TodoStore.getAll()
});
}
createTodo = () => {
TodoActions.createTodo("New Todo");
}
render() {
const { todos } = this.state;
const TodoComponents = todos.map((todo) => {
return <Todo key={todo.id} {...todo}/>;
});
return (
<div>
<button onClick={this.createTodo}>Create!</button>
<h1>Todos</h1>
<ul>{TodoComponents}</ul>
</div>
);
}
}
Redux編
Reduxの概要
今から始めるReact入門 〜 Redux 編 immutability とは - Qiita
状態管理コンテナとかいうもので、これもReactとは別のライブラリとして提供されている
状態管理などのビジネスロジックをReactの代わりに管理させることで複雑さを回避する
https://gyazo.com/020e4682103ad678527ee46990010250
Reduxを分かりやすく解説してみた | フューチャー技術ブログ
One Store:fluxと異なりStoreは1つ。immutableで、常に新しく生成されたデータに置換
State:One Storeの中にある状態。
View:状態をレンダリングする所。今回はReactだが、他のFWでも良いし、PlainJSも可
Presentational ComponentsとContainer ComponentsについてはReactの範疇。一旦割愛
Actions:状態が変更される時発動されるもの。
Reducer:dispatcherが受取ったActionsのtypeに応じてstateを更新するもの。
reduce: 変える、(整理して)変える
immutableを実現するためには、新しいObject/配列を作成するJSの関数が鍵
Object.assign() => spread演算子(ES6)と同様 {...a, name: "Bar"}
Array.prototypeのconcat() / filter() / map() / reduce()
immutableな型を提供してくれるimmutable.js
Redux実践
今から始めるReact入門 〜 Redux 編: Redux 単体で状態管理をしっかり理解する - Qiita
$ npm install --save-dev redux
Reduxの基本的な使い方
code: src/js/client.js
import { createStore } from "redux";
// Reducer: dispatcherが受け取ったActionsのtypeに応じてstatewを更新
const reducer = (state = 0, action) => {
switch (action.type) {
case "INC":
return state + action.payload;
case "DEC":
return state - action.payload;
}
return state;
}
// Store: reducerを実行したりする状態管理オブジェクト
const store = createStore(reducer, 1);
// reducerが実行されたときに実行される関数
store.subscribe(() => {
console.log("store changed", store.getState());
});
// reducerにアクションを渡して実行
store.dispatch({ type: "INC", payload: 10 });
store.dispatch({ type: "DEC", payload: 5 });
userとtweetの2つの状態を管理してみる
code: src/js/client.js
import { combineReducers, createStore } from "redux";
const userReducer = (state = {}, action) => {
switch (action.type) {
case "CHANGE_NAME":
// state.name = action.payload だと複数dispatchで非同期にsubscribeされてしまう
// stateの中身を全て新しいオブジェクトに渡し、変更するところだけ上書きする書き方
state = { ...state, name: action.payload };
break;
case "CHANGE_AGE":
state = { ...state, age: action.payload };
break;
}
return state;
}
const tweetsReducer = (state = [], action) => {
switch (action.type) {
case "ADD_TWEET":
state = state.concat({ id: Date.now(), text: action.payload });
}
return state;
}
// combineReducers: 複数のReducerを1つにまとめてreducersとする
const reducers = combineReducers({
user: userReducer,
tweets: tweetsReducer
});
const store = createStore(reducers);
store.subscribe(() => {
console.log("store changed", store.getState());
});
store.dispatch({ type: "CHANGE_NAME", payload: "Tsutomu" });
store.dispatch({ type: "CHANGE_AGE", payload: 35 });
store.dispatch({ type: "CHANGE_AGE", payload: 36 });
store.dispatch({ type: "ADD_TWEET", payload: "OMG LIKE LOL" });
store.dispatch({ type: "ADD_TWEET", payload: "I am so seriously" });
middlewareを以下のような用途に用いることができる。
reducerで処理するためのJSON データのREST APIからの取得
Reducer の処理に入る前後にログ出力
エラーハンドリング
code: src/js/client.js
import { applyMiddleware, createStore } from "redux";
const reducer = (state = 0, action) => {
switch (action.type) {
case "INC":
state = state + 1;
break;
case "DEC":
state = state - 1;
break;
case "ERR":
throw new Error("It's error!!!");
}
return state;
}
// middlewareはReducerに対して副作用を起こさないよう、actionの中身をいじらないこと
const logger = (store) => (next) => (action) => {
console.log("action fired", action);
// nextがないと次のmiddleware or reducerが発火されず、subscribeも動かない。
next(action);
}
const error = (store) => (next) => (action) => {
try {
next(action);
} catch(e) {
console.log("Error was occured", e);
}
}
// logger, errorというmiddlewareを1つにまとめて
const middleware = applyMiddleware(logger, error);
// storeに適用する
const store = createStore(reducer, 1, middleware);
store.subscribe(() => {
console.log("store changed", store.getState());
});
store.dispatch({ type: "INC" });
store.dispatch({ type: "DEC" });
store.dispatch({ type: "ERR" });
ロギングMiddlewareであるredux-loggerを使ってみる
$ npm install --save-dev redux-logger
code: src/js/client.js
import { applyMiddleware, createStore } from "redux";
import { createLogger } from "redux-logger";
const reducer = (state={}, action) => {
return state;
};
// state, action, next stateがコンソールに出力されるようになる
const middleware = applyMiddleware(createLogger());
const store = createStore(reducer, middleware);
store.dispatch({type: "FOO"});
非同期にdispatchを行うにはredux-thunkを使う。(thunk: ドスン、ズシッ)
$ npm install --save-dev redux-thunk axios
code: dummy_api.js
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*'});
setTimeout(() => res.end('{age: 30, id: 0, name: "foo", age: 25, id: 1, name: "bar"}'), 1000);
}).listen(18080);
$ node dummy_api.js (別ターミナルで)
code: src/js/client.js
import { applyMiddleware, createStore } from "redux";
import axios from "axios";
import { createLogger } from "redux-logger";
import thunk from "redux-thunk";
const initialState = {
fetching: false,
fetched: false,
users: [],
error: null
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case "FETCH_USERS_START":
return { ...state, fetching: true };
case "FETCH_USERS_ERROR":
return { ...state, featching: false, error: action.payload };
case "RECEIVE_USERS":
return {
...state,
fetching: false,
fetched: true,
users: action.payload
};
}
return state;
};
const middleware = applyMiddleware(thunk, createLogger());
const store = createStore(reducer, middleware);
// redux-thunkを使うとdispatchにオブジェクトだけでなく関数も渡せるようになる。
// この中でaxiosによる非同期通信を行う。
store.dispatch((dispatch) => {
dispatch({ type: "FOO" });
dispatch({ type: "BAR" });
axios.get("http://localhost:18080").then((response) => {
dispatch({ type: "RECEIVE_USERS", payload: response.data });
}).catch((err) => {
dispatch({ type: "FETCH_USERS_ERROR", payload: err });
});
});
非同期処理には、redux-thunkではなくredux-promise-middlewareを使う方法もある。
これはaxiosの結果に応じてアクション名を作成し、自動で発火してくれるため簡潔になる。
$ npm install --save-dev redux-promise-middleware
code: src/js/client.js
import { applyMiddleware, createStore } from "redux";
import axios from "axios";
import { createLogger } from "redux-logger";
import promise from "redux-promise-middleware";
const initialState = {
fetching: false,
fetched: false,
users: [],
error: null
};
const reducer = (state = initialState, action) => {
switch (action.type) {
// axiosの結果によって自動で作られるアクション。typeの後ろにPENDING, REJECTED, FULFILLEDがつく。
case "FETCH_USERS_PENDING":
return { ...state, fetching: true };
case "FETCH_USERS_REJECTED":
return { ...state, featching: false, error: action.payload };
case "FETCH_USERS_FULFILLED":
return {
...state,
fetching: false,
fetched: true,
users: action.payload
};
}
return state;
};
const middleware = applyMiddleware(promise, createLogger());
const store = createStore(reducer, middleware);
// dispatchが簡潔になる。
store.dispatch({
type: "FETCH_USERS",
payload: axios.get("http://localhost:18080")
});
他にも非同期Middlewareにはredux-sagaというものもある。
これは処理が複雑になってきたときに簡潔化するのに役に立つ。詳細は割愛。
redux-sagaで非同期処理と戦う - Qiita
Reduxアプリケーションを作成する
今から始めるReact入門 〜 Redux 編: Redux アプリケーションを作成する - Qiita
アプリケーションの多くの部分はGithub参照。
上述のReduxの説明で書いていない部分を以下説明
ボタンを押すと仮想APIからTweetを取得するアプリケーションを作成
react-reduxによってreactとreduxを組み合わせる
$ npm install --save-dev react-redux
code: src/js/client.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import Layout from "./components/Layout";
import store from "./store";
const app = document.getElementById('app');
ReactDOM.render(
<Provider store={store}> // ここでreactとreduxを接続
<Layout />
</Provider>, app);
code: src/js/components/Layout.js
import React from "react";
import { connect } from "react-redux";
import { fetchUser } from "../actions/userActions";
import { fetchTweets } from "../actions/tweetsActions";
@connect((store) => { // reduxを使うことを示すデコレータ。
return {
user: store.userReducer.user,
userFetched: store.userReducer.fetched,
tweets: store.tweetsReducer.tweets,
tweetsFetching: store.tweetsReducer.fetching
};
})
export default class Layout extends React.Component {
componentDidMount() {
this.props.dispatch(fetchUser());
}
fetchTweets = () => {
this.props.dispatch(fetchTweets());
}
render() {
const { user, tweets, tweetsFetching } = this.props;
if (tweetsFetching === true) {
return (<div>fetching...</div>);
}
if (!tweets.length) {
return <button onClick={this.fetchTweets}>load tweets</button>;
}
const mappedTweets = tweets.map(tweet => <li key={tweet.id}>{tweet.text}</li>);
return (
<div>
<h1>{user.name}</h1>
<ul>{mappedTweets}</ul>
</div>
)
}
}
デコレータを含むコードをwebpack + babelで扱うには
babel-plugin-transform-decorators-legacyを導入
code: webpack.config.js
plugins: [
'@babel/plugin-proposal-decorators', {legacy: true}, // class-propertiesの上追記
'@babel/plugin-proposal-class-properties', { 'loose': true },
'react-html-attrs'
]
code: jsconfig.json
// デコレータを使うことに対する警告を消すのに必要
{
"compilerOptions": {
"experimentalDecorators": true
}
}
参考:VSCodeとVetur(TypeScript)でexperimentalDecoratorsの警告が消えない - Qiita
dummy_apiの返すデータの部分を修正
code: dummy_api.js
res.end('{"id": 0, "text": "My first tweet."}, {"id": 1, "text": "Good afternoon."}')
$ node dummy_api.js
saga版
Mobx編
今から始めるReact入門 〜 Mobx 編 - Qiita
Mobx:Reduxよりもシンプルな状態管理ライブラリ
table: 違い
比較観点 Redux Mobx
データ更新タイミング dispatch データが変更されたかを監視(obserbableモデル)
ボイラープレート(同じようなコード) 多い 少ない
値の渡し方の標準 payloadが標準 特に標準はない
学習コスト 高め 比較的低い
コミュニティの大きさ(関連ツール) 大きい 比較的小さい
状態の更新ロジック reducerからだけ actionを使ったりpushを直接呼ぶ
predictability (状態の予測性) 予測しやすい 様々な方向から状態が更新されるため難しい
testability(テスト可能性) テストしやすい 様々な方向から変更があるとテストしにくい
Scalability/Maintainability 良い 複雑になってくるとReduxの方が良い
適しているアプリケーション
MobX
リアルタイムシステム、ダッシュボード
テキストエディタ、プレゼンテーションソフトウェア
イベントベースではない状態の更新を必要とするアプリケーション
Redux
ビジネスアプリケーション
イベントベースなシステム
複雑な反応を含むゲームのイベント
Mobxアプリケーションを作る
$ npm install --save-dev mobx mobx-react
ベースはここ
Mobxのobservable storeの使い方がちょっとちがったのでこちらも参考にした
なぜか@observableで定義したオブジェクトは変更が反映されない
Todoアプリ
code: src/js/client.js
import "../css/index.css";
import React from "react";
import ReactDOM from "react-dom";
import TodoList from "./TodoList";
import store from "./TodoStore";
const app = document.getElementById("app");
ReactDOM.render(<TodoList store={store} />, app);
code: src/js/TodoStore.js
import { observable /*, autorun*/ } from "mobx";
export default store /* = window.store */ = observable({
todos: "buy milk", "buy eggs",
filter: "",
createTodo (value) {
this.todos.push(value);
},
// @computedはgetterメソッドとして定義することでfilteredTodos.map...のようにアクセスできる
get filteredTodos() {
var matchesFilter = new RegExp(this.filter, "i");
return this.todos.filter(todo => !this.filter || matchesFilter.test(todo));
}
})
// storeに変化があった時実行される関数
// autorun (() => {
// console.log(store.filter);
// console.log(store.todos0);
// });
code: src/js/TodoList.js
import React from "react";
import { observer } from "mobx-react";
@observer
export default class TodoList extends React.Component {
createNew = (e) => {
if (e.which === 13) { // 押されたキーがReturn/Enterのとき
this.props.store.createTodo(e.target.value);
e.target.value = "";
}
}
filter = (e) => {
this.props.store.filter = e.target.value;
}
render() {
const { filter, filteredTodos } = this.props.store;
const todoList = filteredTodos.map(todo => (
<li>{todo}</li>
));
return <div>
<h1>todos</h1>
<input class="create" onKeyPress={this.createNew} />
<input class="filter" value={filter} onChange={this.filter} />
<ul>{todoList}</ul>
</div>;
};
}
obserbableの中のobservableであるnested obserbableが使える。
Todoリストアイテムに対してチェックボックスを追加する場合を考える。
code: src/js/TodoStore.js
import { observable } from "mobx";
function createTodo(text) {
return observable({
text: text,
id: Date.now(),
complete: false,
toggleComplete () {
this.complete = !this.complete
}
})
}
export default store = window.store = observable({
todos: [],
filter: "",
createTodo (text) {
this.todos.push(createTodo(text));
},
get filteredTodos() {
var matchesFilter = new RegExp(this.filter, "i");
return this.todos.filter(todo => !this.filter || matchesFilter.test(todo.text));
},
clearComplete () {
const incompleteTodos = this.todos.filter(todo => !todo.complete);
this.todos.replace(incompleteTodos);
}
})
code: src/js/TodoList.js
import React from "react";
import { observer } from "mobx-react";
@observer
export default class TodoList extends React.Component {
createNew = (e) => {
if (e.which === 13) {
this.props.store.createTodo(e.target.value);
e.target.value = "";
}
}
filter = (e) => {
this.props.store.filter = e.target.value;
}
render() {
const { filter, filteredTodos, clearComplete } = this.props.store;
const todoList = filteredTodos.map(todo => (
<li key={todo.id}>
<label>
<input type="checkbox"
// toggleCompleteの中ではthisをtodo自身にしなければならない。
// このbindがないとthisはundefinedになってしまう
// clearCompleteも同様の理由でbindしてある。
onChange={todo.toggleComplete.bind(todo)}
value={todo.complete}
checked={todo.complete} />
{todo.text} (id: {todo.id})
</label>
</li>
));
return <div>
<h1>todos</h1>
<input class="create" onKeyPress={this.createNew} />
<input class="filter" value={filter} onChange={this.filter} />
<ul>{todoList}</ul>
<button onClick={clearComplete.bind(this.props.store)}>Clear Complete</button>
</div>;
};
}
その他便利要素
Material-UI
React版Vuetify的なやつ
Material-UI: A popular React UI framework で色々探せる
$ npm install @material-ui/core
code: index.js
import { Button } from '@material-ui/core';
function App() {
return <Button color="primary">Hello World</Button>;
}