Redux 入門 (1)

今回は React 入門をいったんお休みして、 Redux に入門する。最終的には React & Redux のアプリを作る必要が出てきそうだたので、ここらへんでいったんしっかり勉強したほうがいいかなと。

Getting Started with Redux · Redux

Redux のページを一通り読んで見たがいまいちわからず、Redux の公式 Example を眺めながら実際に動かしてみた。

最終的にはここの記事に書かれていることが行われているんだな。ということで納得いったんだけど、納得するまでに、Exampleのどのファイルがどの役割と結びついているのかがイメージできなくて困ってた。

タスクの追加に関して言うと、User InputView に当たる部分は 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 に変更する。

  • buttonButton
  • ul を `List‘ に
  • liListItem & ListItemText

それぞれ変更していく。

ただ、そのまま実行すると onClick の型違いでエラーが発生するので、エラーの通り修正する。

  • HTMLLIElement を HTMLDivElement に変更
  • React.FormEvent を React.ChangeEvent に変更

それでもまだ問題があって、liListItemListItemText に分けたことで 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> {

成果物

上記をすべて実装した場合、以下のようになる。

f:id:bamch0h:20190128233506g:plain
Material-UI

コード群は以下に挙げているので、興味があればみてほしい。

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.tsxNumberList を呼び出す

import * as React from "react";
import * as ReactDOM from "react-dom";

import { NumberList } from "./components/NumberList";

ReactDOM.render(
    <NumberList />,
    document.getElementById("root")
);

実行結果

これで、npm start を実行すると以下のような画面が表示されるはずだ。

f:id:bamch0h:20190127155109p:plain
NumberListの表示サンプル

動的追加

+ ボタンを追加して、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つ。keyref について。listItems を作成するときに <li> の要素を返すわけだが、keyを指定しないとデバッグツールでワーニング(?)が表示される。その理由は以下の記事が詳しい。

React.jsの地味だけど重要なkeyについて - Qiita

次の ref は 子コンポーネントの参照を自コンポーネントに持つことができる技術で、以下の記事が詳しい。

React Refs with TypeScript – Martin Hochel – Medium

コード中でインポートしている TodoStateInterface は Todo の状態を管理するためのインターフェースで以下のようになっている。

export interface TodoState {
  items: string[]
}

実行結果

上記コードを動かすと以下のようになるだろう。

f:id:bamch0h:20190127211112g:plain
Todo 動的追加

完了処理を追加

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 属性があるので、それで取得した。

実行結果

上記を実行すると以下のような感じになるだろう。

f:id:bamch0h:20190127230727g:plain
Todo 完了処理

まとめ

簡単なTodo アプリを作り始めた。追加と完了は作成できたので、次は削除処理を作成していきたい。また、値が揮発するので永続化もしなければならないだろうし、コンポーネントも1つだけしか使っていないのがあまり設計上よいとはいえない。Redux等を使えばそのあたりは解決できるのだろうか? 後は、実装をするだけしてテストを一切かけていないので、テストも書いていきたい。

今回の成果物

以下に今回の成果物は上げている。興味があれば見てみてほしい。

GitHub - bamchoh/react-study at 2019-01-27

おまけ (2018-01-28 追記)

各TODO要素に削除ボタンを追加したものを作成したので、一応の成果物としてここにリンクを張っておく。 特段記事にすることもないので、リンクだけ。

GitHub - bamchoh/react-study at 2019-01-28_add_del_button

React 入門 (5) ~ React x Typescript x Jest でテスト ~

今回もJestの記事だが、React x Typescript x Jest の組み合わせを書いてなかったな。ということで。

Jest · TypeScript Deep Dive

今回も参考にするのはこの記事。その中の 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 };
  }
  ...

も一度テストを実行すると通った。

おわり。

成果物は以下。

GitHub - bamchoh/react-study at 2019-01-26

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 でテストを実行する。環境は上で作成したものをベースにする。

Jest · TypeScript Deep Dive

上記を参考にする。

$ 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 は特殊なようで、アロー関数で解決できると書いてある。

GitHub - Microsoft/TypeScript-React-Starter: A starter template for TypeScript and React with a detailed README describing how to use the two together.

確かに、ここらへんをみてもアロー関数を使っているので、これが正解っぽい?

ということで、Rect.tsx のその部分を変更

   // この部分
    onIncrement = () => {
        var num = this.state.num + 1
        this.setState({num})
    }

これでボタンを押すと数字がインクリメントする。

完成品は以下

GitHub - bamchoh/react-study at 2019-01-24

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.exportsexternals は グローバルのモジュールを見るための設定らしいので、それを消す。

最終的には以下のような 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"
    })
  ]
};