Redux 入門 (1)
今回は React 入門をいったんお休みして、 Redux に入門する。最終的には React & Redux のアプリを作る必要が出てきそうだたので、ここらへんでいったんしっかり勉強したほうがいいかなと。
Getting Started with Redux · Redux
Redux のページを一通り読んで見たがいまいちわからず、Redux の公式 Example を眺めながら実際に動かしてみた。
最終的にはここの記事に書かれていることが行われているんだな。ということで納得いったんだけど、納得するまでに、Exampleのどのファイルがどの役割と結びついているのかがイメージできなくて困ってた。
タスクの追加に関して言うと、User Input
と View
に当たる部分は AddTodo.js
だろう。ボタンを押すと form
ノードの onSubmit
が発火する。すると、dispatch(addTodo(input.value))
がコールされる。addTodo()
でアクションが作成され、dispatch()
で storeにディスパッチされる。
addTodo()
というのが ActionCreator
のことで、実装はsrc/actions/index.js
にある。内容は単純にオブジェクトを生成してるだけ。生成されたオブジェクトは Action
にあたり、それが dispatch()
関数によって、store にディスパッチされる。
ディスパッチ先のstoreは src/index.js
で作成されている。crateStore()
関数に reducer を渡して store を作成している。作成された store は <Provider>
コンボーネントに渡されることで保持されている。
import React from 'react' import { render } from 'react-dom' import { createStore } from 'redux' import { Provider } from 'react-redux' import App from './components/App' import rootReducer from './reducers' // ここで store が作成される。 const store = createStore(rootReducer) render( // ここで store を保持 <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
reducer は src/reducers
フォルダの下にある。reducer は新たなstate を作成し返すだけの関数で、sub-reducer を combineReducers()
関数を使うことでまとめて一つの reducer とすることができるようだ。
また、AddTodo.js や VisibleTodoList.js は connect() という関数でラップされているが、これは dispatch()
関数や state
をコンポーネントに渡すために必要なものらしい。
成果物
ここに、Todo に追加する部分だけを抽出したものを置いておく。できるだけ単純にしたので最低限のReduxのふるまいを知る分には有用だろう。
GitHub - bamchoh/redux-study at 3b75c828a886b19cdce2122bf1eacdc9f9010edf
React 入門 (7) ~ TODOアプリを作る(2) ~ MATERIAL-UI の導入
UIがかっこいいとコーディングもノッてくるというもの。ということでMATERIAL-UIを入れてみる。
上記を参考にする。
インストール
まずは、必要なパッケージのインストール
$ npm install @material-ui/core
マイグレーション
次に、既存のコンポーネントをMaterial-UI に変更する。
button
をButton
にul
を `List‘ にli
をListItem
&ListItemText
に
それぞれ変更していく。
ただ、そのまま実行すると onClick
の型違いでエラーが発生するので、エラーの通り修正する。
- HTMLLIElement を HTMLDivElement に変更
- React.FormEvent を React.ChangeEvent に変更
それでもまだ問題があって、li
を ListItem
と ListItemText
に分けたことで key
関係のエラーが再発する
key
属性は ListItem に記述して、id
属性は ListItemText に記述するとよい。
削除ボタンの変更
TODO
を削除するボタンは今まで普通のボタンに -
といった味気ないものだったので、これを機にゴミ箱アイコンに変更する。
List React component - Material-UI
ここらへんに良いサンプルがあるので参考にする。
新たにパッケージをインストールする。
$ npm install @material-ui/icons
以下のようにコードを変更する。これを button
と置き換えるだけだ。簡単。
... <ListItemSecondaryAction> <IconButton aria-label="Delete" onClick={this.on_click_for_del} id={String(i)}> <DeleteIcon /> </IconButton> </ListItemSecondaryAction>
マージンの設定
ここまでの実装で実行してみると、TODO追加ボタンとテキスト部分が近く感じる。いい感じにマージンを埋め込みたい。
Button React component - Material-UI
ここらへんのコードサンプルをみてみると、theme.spacing.unit
という値を margin
につけてやればいいようだが styles
を作成するのに手間取った。
Material-UI のガイド > TypeScriptを試す1 - Qiita
この記事を見ると、コンポーネントを丸っとスタイル設定用の関数でくるんでやる必要があるようだった。今回は独自のprops インターフェースは必要なかったので、以下のように定義した。
ちなみに、以下のパッケージが必要になる。
$ npm install @material-ui/styles
// スタイル設定用の関数を定義 const decorate = withStyles((theme: Theme) => { return createStyles({ button: { margin: theme.spacing.unit } }); }); // 定義部分を変更 decorate() 関数でクラスを丸っとラップ const NumberList = decorate( class extends React.Component<WithStyles<'button'>, TodoState> { // ... render() { const { classes } = this.props return ( <div className="App"> <div> // className に button 属性を指定することで、marginが効くようになる <Button color="primary" variant="contained" onClick={this.on_click} className={classes.button}>+</Button> // ...
複数のコンポーネントのスタイルを設定する
上記の設定だと button
のみが有効になる、root
の要素に関してもスタイルを設定したい場合は、WithStyles<'button'>
の部分を変更する必要がある。そのために、decorate関数で使っている withStyles をクラス定義のほうに移してくる必要がある。
// スタイル属性を定義。decorate関数にはしない。 const styles = (theme: Theme) => createStyles({ root: { width: '100%', maxWidth: 360, backgroundColor: theme.palette.background.paper, }, button: { margin: theme.spacing.unit } }); // ラップする側でwithStyles()をすることで、WithStylesにstylesのタイプを指定できるようになる const NumberList = withStyles(styles)( class extends React.Component<WithStyles<typeof styles>, TodoState> {
成果物
上記をすべて実装した場合、以下のようになる。
コード群は以下に挙げているので、興味があればみてほしい。
GitHub - bamchoh/react-study at 2019-01-28_add_material_ui
おわり。
React 入門 (6) ~ TODOアプリを作る(1) ~
今まで作ってきた環境でTODOアプリを作る。
まずは単純に、TODOの登録ができて、TODOを完了させるチェックボックスを付与するくらいの小さいものにする。
静的なリストを作成
少しずつ作って完成に近づけたほうがちょっとした修正もしやすくなるはず。ということで、まずは静的にリストを作成する。
まずはリスト用のコンポーネント src/components/NumberList.tsx
を作成する。
今回は 1~5までの数字を羅列するのみ。
import * as React from 'react'; export class NumberList extends React.Component { listItems = [1,2,3,4,5].map((number) => <li>{number}</li> ); render() { return ( <ul>{this.listItems}</ul> ) } }
次に、src/index.tsx
で NumberList
を呼び出す
import * as React from "react"; import * as ReactDOM from "react-dom"; import { NumberList } from "./components/NumberList"; ReactDOM.render( <NumberList />, document.getElementById("root") );
実行結果
これで、npm start
を実行すると以下のような画面が表示されるはずだ。
動的追加
+
ボタンを追加して、list アイテムを動的に追加できるようにする。
ただ、コンポーネント間で値を同期させるやり方がわからない。
以下のURLを参考に色々試すが、うまくいかない。
というのも、初めはTodo コンポーネントを親コンポーネントにして、ボタンコンポーネントとリストするコンポーネントをTodoコンポーネントの下にぶら下げる設計だったので、ボタンコンポーネントで更新したstateをリストするコンポーネント側に渡す必要があり、stateを参照渡しで渡せればいいのだが、それをやる方法がわからず、ボタンコンポーネントで更新したstateをいったんTodoコンポーネントに渡して、それをリスト側に反映する。といったようなことをしないといけないわけだが、それがどうもうまくいかない。
なので、もうTodoコンポーネントのしたに、ボタンとリストを一括で行う子コンポーネントのみにし、子コンポーネントの中ですべてを行うようにした。それが以下のコードになる。上記で作成した src/components/NumberList.tsx
に追記する形で作成。
import * as React from 'react'; import { TodoState } from "./TodoStateInterface"; export class NumberList extends React.Component<{}, TodoState> { text: string; textRef = React.createRef<HTMLInputElement>(); constructor(props: any) { super(props); this.state = { items: [] }; this.text = ""; } updateState = (state: TodoState) => { this.setState(state) } listItems = (state: TodoState) => { return state.items.map((text, i) => { if(text!="") { return <li key={i}>{text}</li> } return null; }); } on_click = () => { this.state.items.push(this.text); this.textRef.current!.value = ""; this.updateState(this.state); } on_change = (e: React.FormEvent<HTMLInputElement>) => { console.log(e.currentTarget.value); this.text = e.currentTarget.value; } render() { return ( <div> <div> <button onClick={this.on_click}>+</button> <input ref={this.textRef} type="text" onChange={this.on_change} /> </div> <ul>{this.listItems(this.state)}</ul> </div> ) } }
特記事項としては、2つ。key
と ref
について。listItems
を作成するときに <li>
の要素を返すわけだが、key
を指定しないとデバッグツールでワーニング(?)が表示される。その理由は以下の記事が詳しい。
React.jsの地味だけど重要なkeyについて - Qiita
次の ref は 子コンポーネントの参照を自コンポーネントに持つことができる技術で、以下の記事が詳しい。
React Refs with TypeScript – Martin Hochel – Medium
コード中でインポートしている TodoStateInterface
は Todo の状態を管理するためのインターフェースで以下のようになっている。
export interface TodoState { items: string[] }
実行結果
上記コードを動かすと以下のようになるだろう。
完了処理を追加
TODOを追加できるようになったので、次は完了処理を追加する。li
要素をクリックすると文字に取り消し線が入るだけの簡単なもの。
まず、TodoStateInterface.tsx
に完了フラグを追加する string[]
では要素ごとに保持できないので、Item
インターフェースを別途作って、 TodoState
ではその配列を保持する。
export interface Item { todo: string done: boolean } export interface TodoState { items: Item[] }
NumberList.tsx
も修正する。修正するのは以下の2つ
li
要素がクリックされたときのイベントの捕捉done
フラグによる要素レンダリング処理の分岐
上記2点を行ったのが以下のコード
import * as React from 'react'; import { TodoState, Item } from "./TodoStateInterface"; export class NumberList extends React.Component<{}, TodoState> { text: string; textRef = React.createRef<HTMLInputElement>(); constructor(props: any) { super(props); this.state = { items: [] }; this.text = ""; } updateState = (state: TodoState) => { this.setState(state) } on_click_li = (e: React.MouseEvent<HTMLLIElement>) => { var id:number; id = +(e.currentTarget.id) this.state.items[id].done = true this.updateState(this.state); } on_click = () => { this.state.items.push({ todo:this.text, done:false}); this.textRef.current!.value = ""; this.updateState(this.state); } on_change = (e: React.FormEvent<HTMLInputElement>) => { this.text = e.currentTarget.value; } drawItems = (item: Item) => { if(item.done) { return <s>{item.todo}</s> } return item.todo } listItems = (state: TodoState) => { return state.items.map((item, i) => { if(item.todo!="") { return <li id={String(i)} key={i} onClick={this.on_click_li}>{this.drawItems(item)}</li> } return null; }); } render() { return ( <div> <div> <button onClick={this.on_click}>+</button> <input ref={this.textRef} type="text" onChange={this.on_change} /> </div> <ul>{this.listItems(this.state)}</ul> </div> ) } }
ここに来るまでに、少々問題があった。一つ目の問題としては、li
をクリックしたときに飛ばされるイベント MouseEvent<HTMLLIElement>
からとれる情報の中にどのli
要素がクリックされたかの情報がそのままでは取れなかったことがある。key
属性の値が取れればいいのだが、どうもとれなさそうだった。色々調べた結果、最終的に id
属性を追加することにした。次に、id
属性を event オブジェクトからどうやって取得するかが問題となった。結果としては currentTarget
プロパティに id
属性があるので、それで取得した。
実行結果
上記を実行すると以下のような感じになるだろう。
まとめ
簡単なTodo アプリを作り始めた。追加と完了は作成できたので、次は削除処理を作成していきたい。また、値が揮発するので永続化もしなければならないだろうし、コンポーネントも1つだけしか使っていないのがあまり設計上よいとはいえない。Redux等を使えばそのあたりは解決できるのだろうか? 後は、実装をするだけしてテストを一切かけていないので、テストも書いていきたい。
今回の成果物
以下に今回の成果物は上げている。興味があれば見てみてほしい。
GitHub - bamchoh/react-study at 2019-01-27
おまけ (2018-01-28 追記)
各TODO要素に削除ボタンを追加したものを作成したので、一応の成果物としてここにリンクを張っておく。 特段記事にすることもないので、リンクだけ。
React 入門 (5) ~ React x Typescript x Jest でテスト ~
今回もJestの記事だが、React x Typescript x Jest の組み合わせを書いてなかったな。ということで。
今回も参考にするのはこの記事。その中の Example enzyme
という章を参照する。
enzyme
ってのはよくわからんですが、React でテストするときに便利になる感じのライブラリらしい。
必要なパッケージをインストール
$ npm i enzyme @types/enzyme enzyme-to-json enzyme-adapter-react-16 -D
ただ、これだけだと、テスト実行時に enzyme-adapter-reacta-16 の 定義がないと怒られてしまうので、以下のパッケージもインストールする
$ npm i @types/enzyme-adapter-react-16 -D
jest.config.js
に設定を追記
module.exports = { // OTHER PORTIONS AS MENTIONED BEFORE // Setup Enzyme "snapshotSerializers": ["enzyme-to-json/serializer"], // 参考元記事では "setupTestFrameworkScriptFIle" を使用していたが // 廃止予定 ということで新しいほうの記述で書く "setupFilesAfterEnv": ["./src/setupEnzyme.ts"], }
src/setupEnzyme.ts
ファイルの作成
import { configure } from 'enzyme'; import * as EnzymeAdapter from 'enzyme-adapter-react-16'; configure({ adapter: new EnzymeAdapter() });
src/components/checkboxWithLabel.tsx
, src/components/checkboxWithLabel.test.tsx
の作成
ここは参考元と一緒なので割愛
テスト実行
$ npx jest
すると以下のようなエラーがでる。(はず)
FAIL src/components/checkboxWithLabel.test.tsx ● Test suite failed to run TypeScript diagnostics (customize using `[jest-config].globals.ts-jest.diagnostics` option): src/components/checkboxWithLabel.tsx:9:15 - error TS7006: Parameter 'props' implicitly has an 'any' type. 9 constructor(props) { ~~~~~
props
の型指定がないよ。ということなのかな?と思ったので。つける。
import * as React from 'react'; export class CheckboxWithLabel extends React.Component<{ labelOn: string, labelOff: string }, { isChecked: boolean }> { constructor(props: any) { // ここを変えた super(props); this.state = { isChecked: false }; } ...
も一度テストを実行すると通った。
おわり。
成果物は以下。
React 入門 (4) ~ Jest 入門 ~
今回は Jest に入門する。React とは関係ないが、プログラミングをしていくうえでテストフレームワークの環境を作っておくことは最近では必須となってきているので入れておく。
Facebook 製 JavaScript テストツール Jest を使ってテストする ( Babel, TypeScript のサンプル付き ) – PSYENCE:MEDIA
これを参考にする。
今回は一から以前作成した環境に上書きする形ではなく、一から作成してみる。というのも、一度以前の環境の上に作ってみたのだが、ちゃんと動かなかった。そこで一旦まっさらな環境で動作させてみて、動くことを確認してからマイグレーションしていったほうがいいと考えたのだ。
基本的に npm
で実行する。
$ git init $ npm init $ npm install --save-dev jest $ mkdir src
src/hello.js
を作成
function hello(name) { return 'Hello ' + name + '!!'; } module.exports = hello;
テスト用ファイル src/hello.test.js
を作成
var hello = require('./hello'); test('hello("jest") to be "Hello Jest!!"', function() { expect(hello('Jest')).toBe('Hello Jest!!'); }); test('hello("jest") not to be "Hello fukumasuya!!"', function() { expect(hello('Jest')).not.toBe('Hello fukumasuya!!'); });
package.json
を書き直す
{ ... "scripts": { "test": "jest" } }
テストを実行する
$ npm run test
これで一応は実行できた。
次に typescript を使て jest でテストを実行する。環境は上で作成したものをベースにする。
上記を参考にする。
$ npm i jest @types/jest ts-jest --save-dev $ npm i typescript --save-dev
jest.config.js
を作成する。
module.exports = { "roots": [ "./src" ], "transform": { "^.+\\.tsx?$": "ts-jest" }, "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$", "moduleFileExtensions": [ "ts", "tsx", "js", "jsx", "json", "node" ], }
src/foo.ts
を作成
export const sum = (...a: number[]) => a.reduce((acc, val) => acc + val, 0);
src/foo.test.ts
を作成
import { sum } from './foo'; test('basic', () => { expect(sum()).toBe(0); }); test('basic again', () => { expect(sum(1, 2)).toBe(3); });
テストを実行する
$ npm run test
完了
さて、本題。以前作成した環境に Jest を入れる
上記で作成した jest.config.js
./src/foo.ts
./src/foo.test.ts
をコピーする。
package.json
に 以下の項目を追加。もしかしたらいらないかも。
{ ... "test": "jest" }
テストの実行は npm run test
ではだめだった(なぜだろう???)
なので、以下で実行する
$ npx jest
成果物は以下。
GitHub - bamchoh/react-study at 2019-01-25
完了
React 入門 (3) ~ Type Script を使って独自コンポーネントを作成 ~
ReactJSで作る今時のSPA入門(基本編) - Qiita
これを参考に Type Scriptのコンポーネントを作成する。 全部を作成するのは大変なので、ボタンのテキストを更新するコンポーネントを作成するのみとする。 んで、昨日作った環境に追加する形ですすめる。
GitHub - bamchoh/react-study at 0013892889de3364ced52eb9e56c35639764f5f4
まずは以下のように作成して、src/components/Rect.tsx
として作成
import * as React from "react"; interface RectProps { num: number; bgcolor: string; } interface RectState { num: number; } export class Rect extends React.Component<RectProps, RectState> { style = {} constructor(props: RectProps) { super(props) this.state = { num: props.num, }; this.style = { width: 50, height: 50, background: this.props.bgcolor, } } onIncrement() { var num = this.state.num + 1 this.setState({num}) } render() { return ( <button style={this.style} className="square" onClick={this.onIncrement}> Hello {this.state.num} </button> ) } }
src/index.tsx
は以下のように変更
import * as React from "react"; import * as ReactDOM from "react-dom"; import { Rect } from "./components/Rect"; ReactDOM.render( <div> <Rect num={1} bgcolor="#00FF00" /> <Rect num={2} bgcolor="#FF0000" /> </div>, document.getElementById("root") );
ただ、これだとクリックしても onIncrement
が完全には実行されない。デバッグツールで確認すると、state
が未定義になっているようだ。
【TypeScript】thisの使い方にハマった!thisを保持する3つの方法 | Black Everyday Company
この記事によると typescript の this は特殊なようで、アロー関数で解決できると書いてある。
確かに、ここらへんをみてもアロー関数を使っているので、これが正解っぽい?
ということで、Rect.tsx
のその部分を変更
// この部分 onIncrement = () => { var num = this.state.num + 1 this.setState({num}) }
これでボタンを押すと数字がインクリメントする。
完成品は以下
React 入門 (2) ~ Type Script を入れる ~
巷で人気の Type Script を入れてみる
ここを参考にする React & Webpack · TypeScript
まず、react と react-dom の タイプ情報をインストールしておく
$ npm install --save react react-dom @types/react @types/react-dom
typescript 本体をインストール
$ npm install --save-dev typescript awesome-typescript-loader source-map-loader
以下の内容のtsconfig.json
ファイルをプロジェクトディレクトリのルートディレクトリに置く。
{ "compilerOptions": { "outDir": "./dist/", "sourceMap": true, "noImplicitAny": true, "module": "commonjs", "target": "es5", "jsx": "react" }, "include": [ "./src/**/*" ] }
src/components
以下に Hello.tsx
として以下のファイルを作成
import * as React from "react"; export interface HelloProps { compiler: string; framework: string; } export const Hello = (props: HelloProps) => <h1>Hello from {props.compiler} and {props.framework}!</h1>;
src
下に index.tsx
を作成
import * as React from "react"; import * as ReactDOM from "react-dom"; import { Hello } from "./components/Hello"; ReactDOM.render( <Hello compiler="TypeScript" framework="React" />, document.getElementById("example") );
プロジェクト直下に index.html
を作成
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>Hello React!</title> </head> <body> <div id="example"></div> <!-- Dependencies --> <script src="./node_modules/react/umd/react.development.js"></script> <script src="./node_modules/react-dom/umd/react-dom.development.js"></script> <!-- Main --> <script src="./dist/bundle.js"></script> </body> </html>
webpack.cofig.js
を以下のように修正
module.exports = { entry: "./src/index.tsx", output: { filename: "bundle.js", path: __dirname + "/dist" }, devtool: "source-map", resolve: { // Add '.ts' and '.tsx' as resolvable extensions. extensions: [".ts", ".tsx", ".js", ".json"] }, module: { rules: [ { test: /\.tsx?$/, loader: "awesome-typescript-loader" }, { enforce: "pre", test: /\.js$/, loader: "source-map-loader" } ] }, externals: { "react": "React", "react-dom": "ReactDOM" } };
ビルドする。
$ npm run build
index.html を直接開く
終わり
ただこれだと昨日作成した環境では動作しないのでどうにかしないといけない...
TypeScriptでReactを書く – webpackを使った開発環境の構築方法 | maesblog
ここによると、module.exports
の externals
は グローバルのモジュールを見るための設定らしいので、それを消す。
最終的には以下のような webpack.config.js
になる。
const HtmlWebPackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.tsx", output: { filename: "bundle.js", path: __dirname + "/dist" }, devtool: "source-map", resolve: { extensions: [".ts", ".tsx", ".js", ".json"] }, module: { rules: [ { test: /\.tsx?$/, use: { loader: "awesome-typescript-loader" } }, { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }, { test: /\.html$/, use: [ { loader: "html-loader" } ] } ] }, plugins: [ new HtmlWebPackPlugin({ template: "./src/index.html", filename: "./index.html" }) ] };