おじんブログ

Webアプリに関する知見とか雑記です

JavaScript で小数点以下を桁数指定して四捨五入するなら、parseFloat() と toFixed() を組み合わせればよい

TL;DR

第一引数に四捨五入する値を、第二引数に小数点以下の桁数を指定する関数を定義する。

/**
 * Rounding number with digit
 * Example:
 * round(3.14159265359, 2) = 3.14
 * @param {number} value
 * @param {number} digits default : 0
 * @returns {number} rounded number
 */
function round(value, digits = 0){
  return parseFloat(value.toFixed(digits))
}

// Math.E = 3.141592653589793
round(Math.PI, 3)
// expected output: 3.142

round(Math.PI)
// expected output: 3

Math.round(Math.PI)
// expected output: 3

解説

JavaScriptで四捨五入するための関数として Math.round() があった。 ただしこれは、引数として与えられた数字を四捨五入して整数部分しか表示できない。

四捨五入後に、整数部分だけでなく、小数点以下の桁数取り扱う場合は下記の関数を組み合わせる必要がある。

まず toFixed() で桁数を指定して整形する。このとき、戻り値が文字列なので、 parseFloat() によって小数点を含んだ文字列に変換する。

3.141592653589793.toFixed(3)
// expected output: "3.142"
parseFloat("3.142")
// expected output: 3.142

厳密にやる場合はparseFloat() の値に対して Number.isNaN() で判定すれば良いと思われる。

余談

自分がこの方法を調べたとき、元の数字に100を掛けて Math.round() した後に再び100で割る、という方法がかなり出てきて直感的に「なんかスマートじゃない」と思った。

なので、調べてみたところ、Math.jsが提供している round() 関数では自分がやりたいことに近かったので、該当する実装箇所 を参考にした。

他にも参考になりそうな実装があるので数値処理をしたい方は Math.js のソースを読めば良いと思います。

「Reactを自作しよう」をやってみた & Babelの設定の復習

動機

以前から React を自作しよう を見かけてやりたいと思っていたが GW でやっとまとまった時間が取れたのでじっくり取り込んだ。

React をよく個人的に使うので、その設計と実装を経験したほうが React への理解が深まるのでは?と思った。

TL;DR

作業結果は下記のリポジトリにアップした。

https://github.com/ryokryok/practice-diy-react

感想

記事の中で実装したのは下記の React の関数を簡素化したものだった。

  • React.createElement
  • React.useState
  • ReactDOM.render

実際手を動かした見た所感は下記。

  • 本当に 1 からの実装して数百行のコードで React の関数を置き換えるので面白かった
  • /** @jsx Didact.createElement * とコメントを書くだけで JSX の変換関数が置き換わるのを知った
  • Fiber という作業単位によって効率よくレンダリングするというのが理解できた
  • JSX が取りうる構造を想定しながらそれを実装に落とし込む方法が理解できた
    • HTML の要素が親子関係、または兄弟関係を想定するなど
  • どのように差分検出するのか、というのを理解できた
    • 更新か、追加か、削除かなどを判定するためにどのような実装
  • 型がない JavaScript での実装なので、作業中にどのような Object の実装を目指しているのかちょっとわかりにくかった

環境構築

翻訳版にも元記事にも環境構築はなく、悩んだが Babel で JSX を JS に変換したときの挙動を確認する手順があったため、開発用にBabel + Webpack でセットアップして、変換確認用に script を設定した。

Babel のプラグインに関してはただ単純に変換した結果を見たいため、@babel/preset-react だけ入れた。

通常なら "@babel/preset-env" も入れるが今回はパス。

{
  "presets": ["@babel/preset-react"]
}

package.json にて scripts を追加して yarn transform すればJSX を JS の関数に変換した結果が temp ディレクトリーに展開される。

  "scripts": {
    "transform": "babel src -d temp",
    "dev": "webpack serve",
    "build:dev": "webpack --mode=development",
    "build": "webpack --mode=production"
  },

記事中にもあったがコメントで @jsx Didact.createElement と書くと JSX の変換用関数を別のものに指定できる。

(標準では React.createElement が指定される)

公式では下記に記載がある。例として Preact の変換用関数の記載がある。

@babel/plugin-transform-react-jsx

後は Webpack を入れて、 babel-loader で変換するようにするだけ。Webpack の設定は素朴なので詳細はリポジトリを見てください。


自分以外にやってみた人の記事を見ると create-react-app したのを eject していた。

https://blog.shibayu36.org/entry/2021/04/15/173000

参考

プライムデーで買ってよかったもの

プライムデーから2週間ぐらい経過したけど振り返る。 なんだかんだで合計3万円ぐらい買った。基本的に必要なものを買えたので満足。

Amazon Basicの10mm厚のヨガマット

自宅トレーニングの環境を改善するために買った。 ケトルベルを振ったりスクワットするときに足裏の負荷がかなり減って助かる。 今まで4mm厚のヨガマットを使っていたけど、全く別物。 ゴム臭いというレビューがあったが、プランクぐらいの姿勢なら感じない。鼻を近づけて感じるレベル。

www.amazon.co.jp

8畳向け LEDシーリングライト

3年ぐらい前に買ったLED電球の天井照明が暗かったので買った。 6畳の部屋だけど、8畳タイプでも値段が変わらなかったのでこちらを購入。 LEDシーリングライトって初めてなんだけど、最大光量にするとキンキンに明るくて目が覚めるレベル。 基本的には最低光量から1段上げて、1段温色寄りにしている。

www.amazon.co.jp

ソケットレンチ

バイクの整備に使用するので買った。 まだ使っていないけど、パッケージがそのまま収納ホルダーなので気楽に運搬できるの良い。 ケースみたいにひっくり返してソケットが吹っ飛ぶというのもないのは楽。 在庫切れになっているけど売れると思うので再入荷してほしい。

www.amazon.co.jp

Anker 8-in-1 ハブ

Macbookに接続するのに使用するため買った。 正直オーバースペックだが、LANケーブルも挿せるのでWi-Fiの調子が悪くてもそっちで解決できる。 2つのUSB3.0の端子は片方はオーディオインターフェース、片方はHHKBとマウスをつないでいる。

www.amazon.co.jp

Echo Show 5

アラームと作業中の音楽流しのために買った。 かなり低音が太いため、Alexaが話すたびにドスの利いた機械音声が机を震わせる。 半分ですらかなりの音量で、アラーム効果は強い。 毎朝おはようを言うと今日はなんの日か教えてくれる。和む。 Apple Musicを標準の音楽サービスとして登録できるの快適。 ちなみにBluetooth経由でスマホの音楽流すと低音がないスカスカな音になるのでアレクサ~を流してって言う必要あり。

www.amazon.co.jp

SwitchBot Hub Mini

Echo Showと抱合せで買うと安かったので買った。 が、正直Nature Remoと比較するとリモコンのメーカー検出の精度が微妙だったりリモコンUIが実際のものと剥離した感じがあり、Alexaと連携前提にしたほうがいいかも。

www.amazon.co.jp

総括

プライムデーは直前で値上げがあるとか買うものないとか言われるけど、大体は20%OFFでここ1年で最安値だったりするし、欲しい物が安かったら買うの感覚なら何買っても得する。

余談だけど最近は骨伝導イヤホンが気になっている。 音楽流しつつ周りの音を聞けるので職場での作業だったり、インターホン鳴らされたときに反応できるし、物理的に耳に突っ込まないので汚れる心配が減るのがいいなあと思っている。 なぜプライムデーが終わったときにしばらく何も買いたくないと思っていたのになんで物欲が湧くんだ…

www.amazon.co.jp

確実に理解しながらWebpackで構築したReact + TypeScript環境にJestとReact Testing Library を入れる

TL;DR

サンプルコードは下記です。サンプルで必要な箇所以外は極力シンプルにしています。

https://github.com/ryokryok/react-ts-jest-rtl

  • React + TypeScript(with Webpack)
  • Jest(ts-jest)
  • React Test Library

前提

  • 最低限の yarn または npm コマンドの使い方がわかること。
  • JavaScript には Node.js 環境で実行されるものと ブラウザで実行されるものがあるのを知っていること。
  • この記事では基本的な環境構築と簡素なテストのみ実行記載するが、詳細は各自公式ドキュメントを読むこと。

Webpack 環境

前提として、下記のような Webpack 環境で React + TypeScript がビルド&開発用サーバーが立ち上がることを前提とする。

詳細なファイルの内容は下記のリポジトリのコミットを参照。

https://github.com/ryokryok/react-ts-jest-rtl/tree/4620f7be17e8a2cbcbe2af179af751ce9dbaf120

手元に Clone する場合は下記のように checkout すれば各ファイルの詳細が見られます。

git clone https://github.com/ryokryok/react-ts-jest-rtl.git
cd react-ts-jest-rtl
git checkout 4620f7be17e8a2cbcbe2af179af751ce9dbaf120

ファイル構成

.
├── .git
├── .gitignore
├── node_modules
├── package.json
├── public
│   └── index.html
├── src
│   ├── App.tsx
│   └── index.tsx
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
// package.json
{
  "name": "react-typescript-jest",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "clean-webpack-plugin": "^3.0.0",
    "html-webpack-plugin": "^4.3.0",
    "ts-loader": "^8.0.2",
    "typescript": "^3.9.7",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  },
  "dependencies": {
    "@types/react": "^16.9.46",
    "@types/react-dom": "^16.9.8",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "scripts": {
    "build": "webpack --mode production",
    "start": "webpack-dev-server",
    "typecheck": "tsc -p . --noEmit"
  }
}
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: "ts-loader",
            options: {
              transpileOnly: true,
            },
          },
        ],
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".ts", ".js"],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
  ],
};

最低限のテスト実行環境を作成する

まず Jest のみの導入して、最低限のテスト環境を構築する。 今回は TypeScript をコンパイルする必要があるため、 ts-jest @types/jest も導入する。

yarn add -D jest ts-jest @types/jest
# generate jest.config.js
yarn ts-jest config:init

生成された Jest の設定ファイル jest.config.js にて testEnvironment: "jsdom" に書き換える。 今回のテスト対象はブラウザ実行される JavaScript なので、jsdom を指定する。

https://jestjs.io/docs/en/configuration#testenvironment-string

// jest.config.js
module.exports = {
  preset: "ts-jest",
-  testEnvironment: "node",
+  testEnvironment: "jsdom",
};

package.json にテスト実行用 Scripts を追加する。

+    "test": "jest"

Unit Test を実行する

最低限のテスト実行環境が構築できたのでシンプルなコードとテストコードを書く。 デフォルトでは正規表現 **/__tests__/**/*.[jt]s?(x) または **/?(*.)+(spec|test).[tj]s?(x) に合致するファイルに対してテストが実行されるため、それに合わせた命名規則でテストコードを作成する。 Matcher を指定して、想定通りの値が出ているか確認する。 https://jestjs.io/docs/ja/using-matchers

mkdir src/lib
touch src/lib/sample.ts
touch src/lib/sample.test.ts
// src/lib/sample.ts
export function sum(a: number, b: number): number {
  return a + b;
}

export function fibonacci(n: number): number {
  return n == 0 || n == 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
// src/lib/sample.test.ts
import { sum, fibonacci } from "./sample";

describe("index.ts test", () => {
  test("sum test", () => {
    expect(sum(1, 3)).toBe(4);
    expect(sum(2, 5)).not.toBe(4);
  });

  test("fibonacci", () => {
    expect(fibonacci(1)).toBe(1);
    expect(fibonacci(10)).toBe(55);
    expect(fibonacci(20)).toBe(6765);
  });
});

yarn test でテストを実行する。 すると Jest が正規表現に合致するファイル自動的にテストが実行されます。

$ yarn test
yarn run v1.22.4
$ jest
 PASS  src/lib/sample.test.ts
  index.ts test
    ✓ sum test (1 ms)
    ✓ fibonacci (2 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.761 s
Ran all test suites.
✨  Done in 3.96s.

src/lib/sample.test.ts に対して Matcher を書き換えたり、テストが成功しない値に設定したり、import のパスを間違えると、エラー発生箇所を自動的に指摘される。

また yarn test --watch では Watch モードになり、コードを更新すると自動的にテストが再実行される。( webpack-dev-server のように自動更新される。)

テストファイルの置き場所を変更する

テストファイルと実行用ファイルが同一階層上に存在するのは見辛いので、テスト用ディレクトリを作成してそこにテストファイルを置く。 今回はルート直下に test ディレクトリを配置する。

mkdir test
mv src/lib/sample.test.ts test

jest.config.js にはテストファイルを探索するディレクトリを変更するように設定を追加する。

https://jestjs.io/docs/ja/configuration#roots-arraystring

+  roots: ["<rootDir>/test"],

配置を変更した test/sample.test.ts import のパスを書き換える。 ちなみに書き換え前に yarn test を実行すると Cannot find module './sample' or its corresponding type declarations. と表示される。

- import { sum, fibonacci } from "./sample";
+ import { sum, fibonacci } from "../src/lib/sample";

React を テストするためのライブラリを導入する

React コンポーネントをテストするライブラリは複数あるが、公式が推奨している React Testing Library(RTL) を導入する。

yarn add -D @testing-library/react @testing-library/jest-dom

@testing-library/react ではコンポーネントレンダリングや出力のために導入する。

@testing-library/jest-dom では toBeInTheDocument() のような Jest の Matcher を拡張する。これによって、「Document 上に条件に合致するコンポーネントレンダリングされているか」などの確認ができる。

React Test Library の挙動を理解する

RTL の詳細な使い方は下記がわかりやすい。

https://qiita.com/ossan-engineer/items/4757d7457fafd44d2d2f

今回は簡単なコンポーネントを例にテストを実行する。

touch test/App.test.tsx
// src/App.tsx
import React, { useState } from "react";

function useAuth() {
  const [auth, setAuth] = useState(false);
  function handleClick() {
    setAuth(!auth);
  }
  return { auth, handleClick };
}

export default function App() {
  const { auth, handleClick } = useAuth();
  return (
    <div>
      {auth ? (
        <>
          <h1>Welcome</h1>
          <button name="signOut" onClick={handleClick}>
            Sign out
          </button>
        </>
      ) : (
        <>
          <h1>Please sign in</h1>
          <button name="signIn" onClick={handleClick}>
            Sign in
          </button>
        </>
      )}
    </div>
  );
}

これからはテストに変更を繰り返すので yarn test --watch を実行していく。

まずは、RTL ではどのようにコンポーネント評価されるのか確認するためにまずは <App />レンダリングする前にどのような状態になっているか確認するため、 screen.debug() メソッドを実行する。

// test/App.test.tsx
import App from "../src/App";
import "@testing-library/jest-dom";
import { screen } from "@testing-library/react";
import React from "react";

describe("App components", () => {
  test("render App components", () => {
    screen.debug();
  });
});

するとコンソール上に下記のように表示される。

    <body />

ここで render(<App />) を実行する。

import App from "../src/App";
import "@testing-library/jest-dom";
- import { screen } from "@testing-library/react";
+ import { screen, render } from "@testing-library/react";
import React from "react";

describe("App components", () => {
  test("render App components", () => {
+    render(<App />);
    screen.debug();
  });
});

するとコンソール上に <App />document.body 内にレンダリングされる。 div タグが 1 個多いのは、デフォルトの状態では render メソッドで document.bodydiv を Append しているため。

https://testing-library.com/docs/react-testing-library/api#container

    <body>
      <div>
        <div>
          <h1>
            Please sign in
          </h1>
          <button
            name="signIn"
          >
            Sign in
          </button>
        </div>
      </div>
    </body>

RTL を使用したテストでは基本的に DOM の構造に着目して評価を行っている。

実際にテストを書く

src/App.tsx でテストしたいことは下記。

  • 初期状態では Please sign in が表示されている
  • Sign In ボタンを押すと auth が変更され、Welcome が表示される
  • Sign Out ボタンを押すと auth が変更され、再び Please sign in が表示される

結果として下記のようなテストを書いた。

// test/App.test.tsx
import App from "../src/App";
import "@testing-library/jest-dom";
import { screen, render, fireEvent } from "@testing-library/react";
import React from "react";

describe("App components", () => {
  test("render App components", () => {
    const { getByText } = render(<App />);
    //screen.debug();
    expect(getByText(/Please sign in/)).toBeInTheDocument();
    expect(getByText(/Sign in/)).toBeInTheDocument();
    fireEvent.click(getByText(/Sign in/));

    //screen.debug();
    expect(getByText(/Welcome/)).toBeInTheDocument();
    expect(getByText(/Sign out/)).toBeInTheDocument();
    fireEvent.click(getByText(/Sign out/));

    //screen.debug();
    expect(getByText(/Please sign in/)).toBeInTheDocument();
  });
});

screen.debug() では実際の DOM の状態をコンソールに出力するので、入力やクリックなどのアクション後に期待通り DOM 構造が変化しているか確かめることができる。 ただし、実際にテストを実行する際は不要なので、終わったらコメントアウトか消すことを推奨する。

render メソッドではレンダリング結果として様々なプロパティを取得できるが、今回はレンダリングした DOM のテキストを検索するためのプロパティを取得する。

https://testing-library.com/docs/react-testing-library/api#render-result

const { getByText } = render(<App />);

getByText() メソッドの引数に文字列のマッチ条件を与えると合致したテキストを含んだ DOM を返す。

今回の場合、<App /> コンポーネントの中に存在する DOM を返す。

// return HTMLElement of "<h1>Please sign in</h1>"
getByText(/Please sign in/);

toBeInTheDocument() にて「Document に該当する DOM が存在するか」をテストできる。

expect(getByText(/Please sign in/)).toBeInTheDocument();

ユーザーが DOM 対してクリックや入力などのアクションを行ったときは fireEvent を使用する。今回は Sign in ボタンのクリックなので下記のようになる。

// simulate to click the sign in button by a user
fireEvent.click(getByText(/Sign in/));

テストライブラリの組み合わせ

調べてみると、テストライブラリには複数のパターンがあり、目的が違えど下記のように存在する。

  • テストランナーとして Jest, Mocha, Karma ...
  • TypeScript のコンパイルを ts-jest または babel で行うか
  • React コンポーネントのテストライブラリ Enzyme または React Testing Library または Test Renderer を使用するか

目的やプロジェクトに合わせて使用を選定する必要があるが、今回は React の開発元である facebook が公式ドキュメントで触れている Jest React Testing Library を使用した。コンパイル時に型チェックを行うためにts-jest を選定した。

最後に

個人でゼロからコードを書く場合、 console.log デバッグや REPL で書いてうまく行った結果を書くことで済んでしまうが、実際にテストコードを書いてみて、プロジェクトが膨らんだときやリファクタリングするときは個人で開発していても有効だと感じた。

Webpackを一歩一歩確実に理解してReact + TypeScript環境を作る

動機

毎回 npx create-react-app で React アプリを生成していて Webpack の勉強から逃げていたが、いい加減向き合いたいと思ったため。 昔 Webpack に入門しようとしたら難しいという先入観を抱いたので、できるだけシンプルに、一歩一歩づつ入門する。

最終的に、React + TypeScript の環境を作れるようにする。

リポジトリ

各ステップごとにコミットしているので逐次さかのぼってください。

github.com

前提

  • 最低限の yarn または npm コマンドの使い方がわかること。
  • npx create-react-app で React プロジェクトを作成した事があること。

VSCode 使用を前提としています。

Config なしで実行する

最低限の構成で Webpack が何をするのか理解する。

準備

# run at project directory
yarn init -y # generate package.json
yarn add webpack webpack-cli -D
mkdir src
touch src/index.js
touch src/a.js
touch .gitignore
# .gitignore
node_modules/
dist/
// src/index.js
import a from "./a";
a();
// src/a.js
export default () => console.log("function called");

実行

2 つの JavaScript ファイルは import/export で読み込まれている。 これらを Webpack で Bundle(梱包、まとめる)を行うことで依存関係が解決された 1 つの JavaScript を生成できる。

# mode を指定しない場合は自動的に production モードで実行される
yarn webpack --mode production

Hash: 812c3c2bfa797a640163
Version: webpack 4.43.0
Time: 132ms
Built at: 2020-06-18 23:07:37
  Asset       Size  Chunks             Chunk Names
main.js  982 bytes       0  [emitted]  main
Entrypoint main = main.js
[0] ./src/index.js + 1 modules 79 bytes {0} [built]
    | ./src/index.js 26 bytes [built]
    | ./src/a.js 53 bytes [built]
✨  Done in 1.16s.
// dist/main.js
!(function (e) {
  // 省略
  ...
})([
  function (e, t, n) {
    "use strict";
    n.r(t);
    console.log("function called");
  },
]);

Webpack は Config ファイルなしでは dist/main.js にまとめられる。 https://webpack.js.org/configuration/

Out of the box, webpack won't require you to use a configuration file. However, it will assume the entry point of your project is src/index.js and will output the result in dist/main.js minified and optimized for production.

Config ファイルを設定する

Webpack の最低限の働きが理解できたので、次は Config ファイルを設定する。 まずは Bundle するファイル、Bundle で出力されるファイルを明示的に指定する。

準備

webpack.config.js を作成して設定する。設定用の JavaScript オブジェクトを書いて、module.exports すれば良い。

ついでに、npm script を設定して実行を単調なコマンドで実行できるようにする。

touch webpack.config.js
// webpack.config.js
const path = require("path");

module.exports = {
  // 入力元
  entry: "./src/index.js",
  // 出力先
  output: {
    path: path.resolve(__dirname, "output"),
    filename: "bundle.js",
  },
};
// package.json
-  }
+  },
+  "scripts": {
+    "build": "webpack --mode production"
+  }

実行

出力先を変更したので実行すると output/bundle.js に出力される。

yarn build
$ webpack --mode production
Hash: c4b26abe6d1df8044083
Version: webpack 4.43.0
Time: 173ms
Built at: 2020-06-18 23:46:27
    Asset       Size  Chunks             Chunk Names
bundle.js  982 bytes       0  [emitted]  main
Entrypoint main = bundle.js
[0] ./src/index.js + 1 modules 79 bytes {0} [built]
    | ./src/index.js 26 bytes [built]
    | ./src/a.js 53 bytes [built]
✨  Done in 1.63s.

自動更新と開発用サーバーを実行する

現段階では変更があるたびに毎回 yarn build する必要がある。これでは面倒なので、ファイルが保存されるたびに自動的に変更を反映させる。

HTML ファイル内で JavaScript を実行したときの環境を作成する。

準備

開発環境用サーバー& build 用に webpack-dev-serverhtml-webpack-plugin を導入する。

導入する Plugin は webpack.config.jsplugins に追加する。

public ディレクトリーに index.html を作成する。JavaScript が HTML 内で動作していることを確認するするため、実行されると <div id='root'> 内の文字が変わるようにした。

html-webpack-plugin の設定にて template: に作成した HTML のパスを設定する。これによって開発用サーバーが立ち上がると指定した HTML に自動的に JavaScript を注入し、変更があるたびに更新する。

https://github.com/jantimon/html-webpack-plugin#options

yarn add webpack-dev-server html-webpack-plugin -D
mkdir public
touch public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <div id="root">JavaScript will be injected here.</div>
  </body>
</html>
// a.js
const endPoint = document.getElementById("root");
export default function a() {
  endPoint.innerHTML = "JavaScript is injected!";
}
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
  ],
};
// package.json
"scripts": {
  "build": "webpack --mode production",
+  "start": "webpack-dev-server"
},

実行

yarn start で開発用サーバーが http://localhost:8080/ で立ち上がる。 無事 JavaScript が実行されている場合、 JavaScript will be injected here. の箇所が JavaScript is injected! に置き換わっている。 a.js の文字列を書き換えて保存すると自動的に反映される。

また html-webpack-plugin を導入したので yarn build 時に index.html も出力してくれる。これを導入しない場合、 bundle.js しか出力されない。

https://webpack.js.org/plugins/html-webpack-plugin/#root

# launch dev sever at http://localhost:8080/`
yarn start

# build dist/bundle.js and dist/index.html
yarn build

TypeScript を扱えるようにする

現段階では JavaScript しか扱えない。Webpack では様々なファイルを取り扱えるようにするため、各ファイルごとに loader が提供されている。 今回は TypeScript の loader、 ts-loader を導入する。

https://webpack.js.org/loaders/#root

準備

typescriptts-loader を導入する。

TypeScript を実行するときは tsconfig.json も必要なので設定する。 今回は yarn tsc --init で生成したものからコメント行を削除した。 また、tsconfig.json にて "module": "ESNext" を指定すること。(標準では"CommonJS" が指定されている) Webpack の Tree Shaking(未使用の JavaScript を削ってファイルサイズを軽減する機能)を働かせるために必要。

webpack.config.js には ts-loader が拡張子 .ts のファイルに対して働くようにする。 また、 resolve.extensions で拡張子を指定しておくと、その拡張子は import 時に省略できる。

TypeScript ファイルで実行するため JavaScript ファイルの拡張子を .js から .ts に変更する。

yarn add typescript ts-loader -D
# generate tsconfig.json
yarn tsc --init
mv src/index.js  src/index.ts
mv src/a.js  src/a.ts
// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "ESNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true
  }
}
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.ts",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".ts", ".js"],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
  ],
};
// a.ts
const endPoint = document.getElementById("root");

export default function a() {
  // null check for endPoint
  if (endPoint) endPoint.innerHTML = "JavaScript is injected, from TypeScript";
}

実行

先程と同じ。TypeScript は JavaScript に変換され、Webpack によって 1 つの JavaScript ファイルにまとめられる。

# launch dev sever at http://localhost:8080/`
yarn start

# build dist/bundle.js and dist/index.html
yarn build

React + TypeScript を扱えるようにする

最後に、TypeScript 環境で React(TSX)を扱えるようにする。が、実は前項にてほぼ準備は済んでいる。 TypeScript のコンパイラTSX をサポートしているため、設定を少し加えて React のライブラリと型ファイルを読み込むめば済む。

https://www.typescriptlang.org/docs/handbook/jsx.html

準備

React のライブラリと型ファイルを導入する。

webpack.config.js には、entry 、loader 用の testresolve.extensions を変更する。

tsconfig.json には "jsx": "react" を追記するだけ。

yarn add react react-dom @types/react @types/react-dom
touch src/index.tsx
touch src/App.tsx
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  entry: "./src/index.tsx",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".ts", ".js"],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "public/index.html"),
    }),
  ],
};
// tsconfig.json
{
  "compilerOptions": {
    "jsx": "react",
    "target": "es5",
    "module": "ESNext",
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
// App.tsx
import React from "react";

export default function App() {
  return <div>Hello React with TypeScript</div>;
}
//index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

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

実行

前項と同じ。yarn startyarn build

まとめ

というわけで0から順を追ってちょっとづつ追加した。 説明においては「これはおまじないです」というのをできるだけ避けた。

初見ではWebpackのConfigファイルが複雑に見えるかもしれないが、設定を1つ1つ、意味を確認しながら追加すれば難しくなかった。

あとはCSSを読み込むために style-loader を入れたりして作りたいものに合わせてカスタマイズする。

参考

https://mizchi.hatenablog.com/entry/2020/05/03/151022

https://ics.media/entry/16329/

https://webpack.js.org/guides/

useRefを実践例を挙げながら使う

React Hooks で追加されたuseRefは最初は使いどころがよく分からなかったのですが、実践例を見つつサンプルを書いてみたら使いどころが掴めてきたのでまとめます。

前提

ReactのuseStateuseEffectの使い方は覚えたけどuseRefについてはあまり公式例を見てもイメージがつかない人向けです。

サンプルコードはnpx create-react-app my-app --template typescriptで作成して、App.tsxに書いています。

https://create-react-app.dev/docs/adding-typescript

.current を利用して初回レンダーのuseEffect()を早期returnで終了させる

useRefには.currentプロパティがありますが、これはコンポーネントのレンダー時に毎回セットした初期値を返します。 Reactにとってのインスタンス変数のように扱えます。 この性質を利用して初期描画時にフラグを立てて、useEffectのマウント時の初回実行を早期returnで終了させることができます。 これによってStateが更新された時のみuseEffectに設定した関数を実行、というのが実現できます。

下記の例はFooコンポーネント内のstateが更新された時のみuseEffectに設定した関数を実行します。 ブラウザ上のconsole画面を見ながらボタンを押していくと分かりやすいです。

import React, { useState, useRef, useEffect } from "react";

function App() {
  return (
    <div>
      <Foo />
    </div>
  );
}

function Foo() {
  const [value, setValue] = useState(0);

  const isFirstRender = useRef(true);

  const addValue = () => {
    setValue(value + 1);
  };

  const sameValue = () => {
    setValue(value);
  };

  useEffect(() => {
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    console.log("updated");
  }, [value]);
  return (
    <>
      <h3>{`Now state : ${value}`}</h3>
      <button onClick={addValue}>+1</button>
      <button onClick={sameValue}>don't change</button>
    </>
  );
}

export default App;

isFirstRender.currentによる初回実行時の早期returnがない場合、初回マウント時にconsole.log("updated");が実行されてしまいます。

  useEffect(() => {
    // block exec when first render
    if (isFirstRender.current) {
      isFirstRender.current = false;
      return;
    }
    console.log("updated");
  }, [value]);

createRefのHooks版として使用してrefオブジェクトにアクセスする

これは公式でも触れられていたものです。これはcreateRef()でも実現できます。

https://ja.reactjs.org/docs/hooks-reference.html#useref

ただ、Hooks時代のReactから入門した人からするとこの説明だけだとfocus()だけなのか?になるので、深掘りして行きます。

公式ドキュメントのRef と DOMの項を見るとこう書いてあります。

いつ Ref を使うか

Ref に適した使用例は以下の通りです。

  • フォーカス、テキストの選択およびメディアの再生の管理
  • アニメーションの発火
  • サードパーティの DOM ライブラリとの統合

https://ja.reactjs.org/docs/refs-and-the-dom.html

フォーカス、テキストの選択とあるのでサンプルコードを書いてみます。 この例ではinputタグの値をコピーしてクリップボードに保存する処理になります。 ここではtargetRef.currentのオブジェクトはHTMLInputElement型なので、HTMLInputElementのオブジェクトが使えます。

import React, { useRef } from "react";

function App() {
  const targetRef = useRef<HTMLInputElement>(null);

  const copyFromRef = () => {
    if (targetRef) {
      targetRef.current?.focus();
      targetRef.current?.select();
      document.execCommand("copy");
      targetRef.current?.blur();
    }
  };

  return (
    <div>
      <button onClick={copyFromRef}>Copy input value</button>
      <input ref={targetRef} type="text" defaultValue="copy to clipboard" />
    </div>
  );
}

export default App;

例えば、会員登録の入力フォームにて、ボタンを押したら自動的に最初の入力項目にフォーカスするようにしてみたい場合を考えます。 その場合はPropsとしてRefオブジェクトを受け取ってフォーカスするボタンコンポーネントを設計した方が、取り回しが良いと思います。 その場合のサンプルコードは下記です。TypeScriptでの型情報を加えると下記の通りです。

import React, { useRef } from "react";

function App() {
  const targetInputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      <FocusButton target={targetInputRef} />
      <input ref={targetInputRef} type="text" defaultValue="focus" />
    </div>
  );
}

interface FocusButtonType {
  target: React.RefObject<HTMLInputElement>
}
function FocusButton(props: FocusButtonType) {
  const focusToTarget = () => {
    if(props.target) {
      props.target.current?.focus()
    }
  }
  return (
    <button onClick={focusToTarget}>Focus!</button>
  )
}

export default App;

アニメーションの発火の例では、こちらが参考になります。

CSSアニメーションの制御を行っています。

PIXIV TECH FES.のLPを支えるCSSアニメーションテクニック

https://inside.pixiv.blog/2020/01/21/180000

まとめ

最初にuseRefを見たときはユースケースが思い浮かばず、必要性を感じなかったのですが、実践例を見てイメージがついて行きました。 createRef()でできたことは置き換えられるみたいなので、Hooks時代から入った人はその辺りを見ながら参考にするといいかもしれません。

参考リンク

Make React useEffect hook not run on initial render

https://stackoverflow.com/questions/53253940/make-react-useeffect-hook-not-run-on-initial-render

useRef は何をやっているのか

https://qiita.com/uhyo/items/246fb1f30acfeb7699da#useref

HTMLInputElement(MDN)

https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement

2年半プログラム学んできた履歴ともっと早めに知りたかったこと

toyokeizai.net

プログラミング教育が学校教育でも始まるということや、人手不足だからスキルを身につけると稼げるとか、未経験から数ヶ月でエンジニアにとか、プログラミングが話題になってますね。

学習していた記録を思い出しながら早めに知りたかったことや詰まった点を思い出していく。

何をやってきたのか

25歳の時に転職したかったのでそこから本格的に始めて、趣味ではWebのフロントエンド/バックエンドを書いて、仕事ではVBAフレームワークプラグインを書いたりしてます。それ以外だと中学生の時にHTMLとCSSで使って個人サイトを作っていました。

本格的にやり始めてから最近までの履歴はこんな感じです。

  • 転職のためにプログラミングスクールでJavaECサイト作成
  • RailsのためにRubyを学習してWebフレームワークでMVCやHerokuを使ったデプロイを学ぶ
  • フロントエンドの開発に興味が出てVue.jsを学ぶ
  • Reactを書き始めてComponent志向や状態管理の必要性を学ぶ

初めての言語のJava(半年ぐらい)

無料のプログラミングスクール(実質は自習会みたいな感じ)に入って言語選択するとき、求人が多かったから、という理由でとりあえずJavaを学ぶ。

詰まった点は

  • 引数と戻り値を使うタイミング(関心の分離)
  • 同一クラス内の関数と別クラスの関数(スコープと呼び出しと設計面)
  • 環境構築(Javaが悪い)
  • 自分が詰まっている場所の言語化とググった結果の取捨選択
  • MySQLJavaをライブラリでつなぎこみ

引数と戻り値は分かったけど、「繰り返し動作をまとめる」だけでなく「関心を分離させて可読性を上げる」という観点まで飲み込むのに時間がかかった。

スクールの方針が「フレームワークを使うと手抜きに見られたり、技術力がないと思われる」という方針のため2017年にJSPServletというレガシーなものを使っていたせいで情報が少なすぎて厳しかった。

今でもJavaはプログラミング初心者が扱うにはハードルが高すぎると思っていて「とにかく動く」「ライブラリを使ってみる」というのがやり辛いのでAndroidアプリを作るとかじゃない限りは開発コスパが悪いと思う。

NullPointerExceptionはガッで直らないことも学んだ。

Web開発のためのRuby(1年程度)

昔はTwitterRailsで作られていたと聞いてRuby on RailsRubyの学習と並行して学ぶ。

このあたりから「どのOSでもインストールせずに動作するWebアプリって開発コスパ高いな」ってことでWebアプリ開発を中心に学んでTodoアプリとか筋トレ記録アプリとか作って壊してました。

rails new my-appコマンドを打ったらドカッとファイルが作られてrails sと打ったら一気にブラウザで動くWebアプリが動作してすっげー!ってなった思い出がある。

詰まった点は

  • Railsの問題なのかRubyの問題なのか(並行して学習したため、どっちの問題か分からなかった)
  • データベースの正規化が分からず、RDBMSに配列や重複する値を突っ込む
  • 開発環境と本番環境の違い(データベースや画像のアップロード用ストレージ)
  • ライブラリ1つ追加しただけで大混乱

少し前に「今どきのスクールはRailsは教えるけどRubyを教えないから使い物にならないエンジニアが量産される」というのが言われていたけど、初心者がいきなりRailsを学ぶと魔法みたいに見えてしまうのでその中間を想像したり、MVCの責務を意識するのは難しいと思います。

Railsも大量のライブラリや自作クラス・メソッドの積み重ねで作られているので、自分の手で単品のライブラリでHTTPリクエストをしてみるとか、画像処理とか、そういうのをやればよかったと思う。

フロントエンドのためのJavaScriptとNode.js(1年ぐらい)

このあたりでだいぶこなれて来て、GitHubのライブラリの説明を見ながらライブラリを組み合わせてミニツールを作るぐらいはできてきた。

プログラムも「Node.jsで書いたコードは、Node.jsをインストールした実行環境で解釈され、OSのコアな部分を利用しながら動作していく」という意識するようになった。

詰まった点は

  • フロントエンドとバックエンドで分ける意義の理解
  • Railsで凝り固まった偏見の脱却
  • ブラウザ実行するJavaScriptとNode.jsで実行されるJavaScriptの違い
  • Callback
  • ES6以降のJavaScript(スプレッド演算子やasync/await)

Railsの密結合のアーキテクチャに慣れていたので、バックエンドをJSON形式のAPIサーバー化させてフロントエンドでAPIを叩いて整形する、というのは手間だと思ったけど、Webアプリで始めたサービスをスマホアプリでも展開する場合はそういうアーキテクチャだと便利だろうなというのは企業の採用例を見ながら理解した。

ES6以降のJavaScriptで追加された構文が糖衣構文と聞いてもピンとこないのでひたすらNode.jsのREPLで実行しながら理解した。

早めに知りたかったこと

楽しみながら自分のペースで進めること

「未経験から数ヶ月で転職」という投稿と自分を比較して成長速度の遅さに嫌気がさしてしまった時がありました。 理解力の早さは個人差が強いので、相対的な早さよりも、自分が理解した事柄を意識した方がいいです。

一気に複数の新しいことをやらない=新しいことは小さく始める

新しい概念やライブラリを理解するときは、出来るだけ他の依存がない、シンプルな環境で挙動を確認してみます。 俗に言う素振りです。使い方を覚えたら出来る範囲でいじり倒して、徐々に習得したい実装に近づけていきます。

Typo機械的なツールで減らす

人間は間違える生き物なので、機械的なツールに依存できる場合はそれに頼ります。そうすることで余計な仕事を減らして本質的な部分に集中できます。

単純なスペルミスはCheckerを、コード整形や構文エラーはPrettierやESLintを頼ります。

サンプルコードをいきなり写生せずコピペして確認

コードを書く練習には写生がいいと聞いたときはググって出てきたサンプルコードをひたすら写生しましたが、殆どがtypo探しになってたのでやめました。 なので先にサンプルコードをコピペしてそのコード自体が動く事を確認して、徐々に挙動を変えて確認しながら働きを理解します。

飽きたり詰まったら、別の言語やツールをやってから戻る

学習した時は分からなくても別の概念やツールを学習したうえで再び取り組むとすんなり理解できる事があります。 ActiveRecordはどうやってデータを取り扱っているのか分からなかったのですが、MySQLを学んでから読むと「こんな感じのSQL文を実行してそう」という勘所が身に付いてきます。

まとめ

プログラミングで詰まった点を自分で解決できない時点でプログラミングに向いていないのでは、というのはある意味真だとは思うのですが、習得する順序が間違っていたり、問題の言語化能力がまだ身についていないだけの可能性もあり、安易に「向いてない」と断ずるのは判断が早いと思っています。

また「作りたい物が無いとモチベーションを維持しづらいのでプログラミングは上達しない」というのもよく言われますが、別にオリジナルでは無くて既存の物の劣化コピーでもいいですし、サンプルコードを別の書き方を考えるのでもいいと思います。 「測量のために計算する」のが好きなタイプもいれば、単純に「数式を解く事自体」が好きなタイプもいると思うので。