リモート開発メインのソフトウェア開発企業のエンジニアブログです

React.jsのコードをTypeScript化する

割と簡単にTypeScript化できることが分かりましたので、既存プロジェクトのReact.jsコードに適用してみました。

開発環境

  • laravel 5.7
  • react 16.2
  • typescript 3.3

パッケージのインストール

TypeScript関連のパッケージをインストールします。

$ cd laradock/
$
$ docker-compose up -d nginx mysql
Starting laradock_mysql_1            ... done
Starting laradock_docker-in-docker_1 ... done
Starting laradock_workspace_1        ... done
Starting laradock_php-fpm_1          ... done
Starting laradock_nginx_1            ... done
$
$ docker exec -it laradock_workspace_1 /bin/bash
$
$ npm install --save-dev typescript ts-loader react react-dom @types/react @types/react-dom
$

tsconfig.jsonの生成

プロジェクト配下のnode_modulesにtscコマンドがあれば実行します。直接ファイル作成でも大丈夫です。

$ ./node_modules/.bin/tsc --init

プロジェクト直下に生成されたtsconfig.jsonを以下のように編集します。

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "jsx": "react",
        "strict": true,
        "experimentalDecorators": true,
        "allowJs": true,
    },
    "exclude": [
        "node_modules",
        "vendor"
    ]
}

webpack.mix.jsの修正

既存のlaravelのwebpack.mix.jsファイルを以下のように修正します。

const mix = require('laravel-mix');

mix.react('resources/ts/app.tsx', 'public/js')
   .sass('resources/sass/app.scss', 'public/css')
   .webpackConfig({
      module: {
         rules: [
            {
               test: /\.tsx?$/,
               loader: 'ts-loader',
               exclude: /node_modules/,
            },
         ],
      },
      resolve: {
         extensions: ['*', '.ts', '.tsx'],
      },
   });

ディレクトリ名、ファイル名の変更

各名称および拡張子をjsからtsに変更します。

変更前

myproject/
  └ resources
     └ js
        ├ components
        │  └ Article.jsx
        ├ models
        │  └ Todo.js
        └ app.js

変更後

myproject/
  └ resources
     └ ts
        ├ components
        │  └ Article.tsx
        ├ models
        │  └ Todo.ts
        └ app.ts

既存のコードに型を追加

TypeScriptの形式に合わせて.ts、.tsxのコードに型を追加していきます。any型はできる限り使わないようにしましたが、使わざるを得ない場面もあると思います。

以下のような感じで型を追加します。

// 修正前:

title = ''
finished = false

constructor(title) {
    this.title = title
}
 
// ↓ ↓ ↓ ↓

// 修正後:

title: string = ''
finished: boolean = false

constructor(title: string) {
    this.title = title
}

サンプルコードを置いておきますので参考になれば幸いです。修正前のTodoApp.jsxについては以下で詳しく説明されています。

https://qiita.com/zaburo/items/fbcdd73d8d707357c25f

TodoApp.jsx(修正前)

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const axios = require('axios');

function RenderRows(props) {
    return props.todos.map(todo => {
        return (
            <tr key={todo.id}>
                <td>{todo.id}</td>
                <td>{todo.title}</td>
                <td><button className="btn btn-secondary" onClick={() => props.deleteTask(todo)}>完了</button></td>
            </tr>
        );
    });
}

export default class TodoApp extends Component {
    constructor() {
        super();
        this.state = {
            todos: [],
            todo: ''
        }
        this.inputChange = this.inputChange.bind(this);
        this.addTodo = this.addTodo.bind(this);
        this.deleteTask = this.deleteTask.bind(this);
    }

    componentDidMount() {
        axios
            .get('/api/get')
            .then((res) => {
                this.setState({
                    todos: res.data
                });
            })
            .catch(error => {
                console.log(error)
            })
    }

    inputChange(event){
        switch(event.target.name){
            case 'todo':
                this.setState({
                    todo: event.target.value
                });
                break;
            default:
                break;
        }
    }

    addTodo(){
        if(this.state.todo == ''){
            return;
        }
        axios
            .post('/api/add', {
                title: this.state.todo
            })
            .then((res) => {
                this.setState({
                    todos: res.data,
                    todo: ''
                });
            })
            .catch(error => {
                console.log(error);
            });
    }

    deleteTask(todo){
        axios
            .post('/api/del', {
                id: todo.id
            })
            .then((res) => {
                this.setState({
                    todos: res.data
                });
            })
            .catch(error => {
                console.log(error);
            });
    }

    render() {
        return (
            <React.Fragment>
                {/* add from */}
                <div className="form-group">
                    <label htmlFor="todo">新規Todo</label>
                    <input type="text" className="form-control" name="todo" value={this.state.todo} onChange={this.inputChange}/>
                </div>
                <button className="btn btn-primary" onClick={this.addTodo}>登録</button>
                {/* table */}
                <table className="table">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>タスク</th>
                            <th>完了</th>
                        </tr>
                    </thead>
                    <tbody>
                        {/* 行の描画 */}
                        <RenderRows
                            todos={this.state.todos}
                            deleteTask={this.deleteTask}
                        />
                    </tbody>
                </table>
            </React.Fragment>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('todoApp'));

TodoApp.tsx(修正後)

// import React, { Component } from 'react';
// import ReactDOM from 'react-dom';
import * as React from 'react';
import * as ReactDOM from 'react-dom';

const Axios = require('axios');
// import Axios from 'axios';

interface TodoAppProps {
}
interface TodoAppState {
    todos: [{id:string, title:string}],
    todo: ''
}

function RenderRows(props: any) {
    return props.todos.map((todo: {id: string, title: string}) => {
        return (
            <tr key={todo.id}>
                <td>{todo.id}</td>
                <td>{todo.title}</td>
                <td><button className="btn btn-secondary" onClick={() => props.deleteTask(todo)}>完了</button></td>
            </tr>
        );
    });
}

export default class TodoApp extends React.Component<TodoAppProps, TodoAppState> {
    constructor(props: TodoAppProps) {
        super(props);
        this.state = {
            todos: [{id: '', title: ''}],
            todo: ''
        }
        this.inputChange = this.inputChange.bind(this);
        this.addTodo = this.addTodo.bind(this);
        this.deleteTask = this.deleteTask.bind(this);
    }

    componentDidMount() {
        Axios
            .get('/api/get')
            .then((res: { data: [{ id: string, title: string }] }) => {
                this.setState({
                    todos: res.data
                });
            })
            .catch((error: string) => {
                console.log(error)
            })
    }

    inputChange(event: any){
        switch(event.target.name){
            case 'todo':
                this.setState({
                    todo: event.target.value
                });
                break;
            default:
                break;
        }
    }

    addTodo(){
        if(this.state.todo === ''){
            return;
        }
        Axios
            .post('/api/add', {
                title: this.state.todo
            })
            .then((res: { data: [{ id: string, title: string }] }) => {
                this.setState({
                    todos: res.data,
                    todo: ''
                });
            })
            .catch((error: string) => {
                console.log(error);
            });
    }

    deleteTask(todo: { id: string, title: string }){
        Axios
            .post('/api/del', {
                id: todo.id
            })
            .then((res: { data: [{ id: string, title: string }] }) => {
                this.setState({
                    todos: res.data
                });
            })
            .catch((error: string) => {
                console.log(error);
            });
    }

    render() {
        return (
            <React.Fragment>
                {/* add from */}
                <div className="form-group">
                    <label htmlFor="todo">新規Todo</label>
                    <input type="text" className="form-control" name="todo" value={this.state.todo} onChange={this.inputChange}/>
                </div>
                <button className="btn btn-primary" onClick={this.addTodo}>登録</button>
                {/* table */}
                <table className="table">
                    <thead>
                        <tr>
                            <th>ID</th>
                            <th>タスク</th>
                            <th>完了</th>
                        </tr>
                    </thead>
                    <tbody>
                        {/* 行の描画 */}
                        <RenderRows
                            todos={this.state.todos}
                            deleteTask={this.deleteTask}
                        />
                    </tbody>
                </table>
            </React.Fragment>
        );
    }
}

ReactDOM.render(<TodoApp />, document.getElementById('todoApp'));

まとめ

コーディング時に型エラーをチェックしてくれるTypeScriptは大変便利ですので、できるかぎりJavaScriptから置き換えていきたいところです。

← 前の投稿

Laravel の eager loading に関する小ネタ

次の投稿 →

S3とCloudFront関連のあれこれ

コメントを残す