おじんブログ

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

確実に理解しながら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文を実行してそう」という勘所が身に付いてきます。

まとめ

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

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

雑な仕事観: 向いていないことは仕事にしないほうがいい

仕事観と書くと身構える人が多いのでラフにした。 こういう話はキャリアステップを狙う人だけの話、と考える人もいるけど仕事と趣味を分けてて充実させたい人でも、むしろそういう人ほど、向いていないことを考えた方がいい。

向いていないこととは

ここで言う向いていないこととは、それを続けていて辛くないということ。別に一流になれなくてもいいけどやるのにそんなに労力を必要としない、という意味。

例えば喋るのが苦手な人は漫才師やコールセンターはできたとしてもかなり限定的で労力がかかる。これは「向いていない」といえる。

逆に機械いじりが好きな人は業務用エアコンやプリンターの整備業務は続けても苦にはならないと思う。これは「向いている」といえる。

なんで向き不向きと仕事が関係するのか

そもそも大半の人は1日の大半を仕事して過ごす。その時間を適正のないことに費やすのは、仮に仕事と割り切っても労力がいる。

1日に使えるエネルギーが10なら、適正がある人なら6で仕事を済ませて4を余暇や生活に使えるとする。

しかし、向いていない人は8または9、酷いときは11を費やしてしまう。残ったエネルギーは僅か、またはマイナスなのでそれを休日の睡眠で解消する。

つまり適正を考慮しないと、余暇を楽しむどころか生活が破綻しかねない。なので趣味を充実させたい人は、最小の労力で済ませられるほうがいい。

また、昇給したいと思うには自分の能力を高めることが必要で、そういう戦略を考えたり実行するにも適正があったほうが苦になりにくい。

なんでこんなことを書いたのか

僕自身が最初の職場で仕事が上手くできず、20代で2回転職する事になり、仕事の適性について突きつけられて考えざるを得なかったからである。

僕の場合は自分の興味がないことは一切といっていいほどに頭に入らなかった。そういう形質は薄々自覚していた。学校の成績がかなり凹凸があった。 そういう形質を自覚していながら、仕事に役立つようなスキルを身につける気はなかった。ただ単に学業で満足していた。

就活では、自分は営業職に向いていないと自覚していながら、就活期間終盤であることや興味のある範囲に近いという理由で特殊な営業職に就職したが、向いていないことをやり続けていたせいか周りに迷惑をかけていることによる罪悪感が貯まりすぎて、不眠とカフェイン中毒が悪化した。

その時は、仕事なんだから、それぐらいできないのは自分が駄目だから、とにかく頑張ろう、という曖昧な形で問題を押し込んでしまい、特に適正を分析をすることもなかった。 それが原因で徐々に精神的に疲弊してしまい、生活に支障をきたすようになった。体重は2年で10kg増えた。

その時の体験があって、仕事における向き不向きは強く意識するようになった。

最後に

始めたきっかけは「当時の職場から離れるため」という不憫な理由で始めたプログラミングも、元々凝り性で中学生の時にHTMLを触っていたような自分には合っていたようで、理解できなかったことが理解できるようになっていき、いくらやっても楽しいと感じられた。 これが「向いている」なんだと感じて、今のエンジニア職をやれている。 「好きなこと」「向いていること」「稼げること」が運良く一致していたのでバイアスが掛かっているかもしれないが、「向いていないこと」はハンディキャップとして常に重りを付けて全力疾走させられるような感覚だと思っているので、もしそういう状況に心当たりがあるなら、適職を考えたほうが良いと思う。

React 360を開発するときに引っかかりやすい点

開発環境と他のサンプルページ見ていろいろ気が付いたこと。

ChromeではHttps環境でないとデバイスの傾きが感知できない

サンプルページではスマートフォンでみると傾きが反映されていたのに、 開発環境で立てたローカルネットワークにアクセスしてみると、デバイスの傾きが反映されていなかった。 window.DeviceOrientationEventが何らかの原因で働かないんだろうなと思い、調べてみたところ、下記のIssueで答えが出ていた。

github.com

Chromeではセキュリティーの問題でHttps環境でないと傾きを検知できない。

じゃあNetlifyにホスティングしてみるか…と思ったら引っかかりポイントがまた増えた。

ビルド結果が黒背景⇨buildディレクトリにstatic_assetsがない

特に考えずビルドしてフォルダ指定すればNetlifyへのデプロイ大丈夫っしょ、と思ったらダメだった。 よくみると公式で「static_assetsディレクトリに何かファイルあるならそれもコピーしてね」と書いてある。

https://facebook.github.io/react-360/docs/publish.html

というわけでpackage.jsonのscriptを一部を書き換え。static_assetsをbuildディレクトリに含める。

    "bundle": "node node_modules/react-360/scripts/bundle.js && cp -rp ./static_assets ./build/ ",

Netlifyであとは下記のように設定。

  • Build command: yarn bundle

  • Publish directory: build

これでHttps環境でデバッグできる。Netlifyは本当に楽にデプロイできるし早いのでありがたい。

React 360ではまだHookは使えない

TL;DR

React 360にて依存しているReactとReact NativeがHookに対応する前のバージョンのため、React Hookはまだ使えない。Issueには上がっている。

github.com

github.com

package.json も見てみると確かに対応する前のバージョンになっている。

  "dependencies": {
    "react": "16.3.2",
    "react-native": "~0.55.4",
    "three": "^0.87.0",
    "react-360": "~1.1.0",
    "react-360-web": "~1.1.0"
},

気付いたきっかけ

WebVRを試すために、React経験者なら入門しやすそうなフレームワークとしてReact 360を触れてみたけど、カウンターアプリのサンプルがClass Componentだったので、Functional ComponentでuseState使って書き換えてみてロードした時点で他のcomponentがレンダリングされなくなった。 検索したら上記のIssueが見つかった。

将来的にどうなるか?

React 360の最新リリースが2018/5/17だったので最新版のReactに対応するのは望み薄いかな?とは思った。

Release React 360 - v1.0.1 · facebook/react-360 · GitHub

が、コアコミッターの人が「WebVR 1.1 APIはレガシーだからWebXR APIに注力して行くよ」と言っているみたいなので、アップデートというよりガラッと中身を変えていきそう。

github.com

そもそも複数componentで個別のState持てない?

ついでに色々試してみる。 チュートリアルのCounterだけ分離してみる

https://facebook.github.io/react-360/docs/explore-code.html

import React from "react";

import { AppRegistry, StyleSheet, View, Text, VrButton } from "react-360";

export class Counter extends React.Component() {
  state = {
    count: 0
  };

  _incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <VrButton onClick={this._incrementCount} style={styles.greetingBox}>
        <Text style={styles.greeting}>{`Count: ${this.state.count}`}</Text>
      </VrButton>
    );
  }
}

export class Sample360 extends React.Component {
  render() {
    return (
      <View style={styles.panel}>
        <Counter />
      </View>
    );
  }
}
// style は省略
AppRegistry.registerComponent("Sample360", () => Sample360);

実行コンソール見るとCounterを定義したあたりでエラーが出てる。Counter周りを消してみると無事Panelは表示される。 カウンターをSample360内で定義すると動作するので、Stateを含むComponentを複数持てないのか…? よくわからない…

まとめ

React 360は近々大きなアップデートがありそうな予感がしている。ただ現状ではWebVRを踏み込んでやるなら他の選択肢が良さそう。 Three.jsでいじっていた方が難易度は上がるけど自由にできるみたいで、開発頻度的に活発そうなフレームワークはA-Frame、みたいな印象がある。 まだ手を動かしてないしドキュメントも読んでいないので認識が間違っていそう。 また気が向いたらWebVR/WebXRで遊んでみたい。