😇

JSXを使わないReactチュートリアル

2022/10/11に公開

JSXを使わないReact公式チュートリアル

よくJSXが嫌いという人をTwitterなどで見かけますが、JSXがないとどうなるか実際にReact公式チュートリアルを用いて体験してみました。
https://ja.reactjs.org/tutorial/tutorial.html

※ 筆者は特にJSXが嫌いというわけではなく、むしろシンタックスシュガーとしての素晴らしい拡張だと思っています。

create-react-app はbabelによって、jsでもjsxが使えるように勝手にトランスコンパイルしてくれるので、これは使わずにbabelなしでやっていきます。

環境構築

## package.jsonの作成
$ npm init -y

## webpackのインストール
$ npm install -D webpack webpack-cli webpack-dev-server @webpack-cli/generators
$ npx webpack-cli init

? Which of the following JS solutions do you want to use? none
? Do you want to use webpack-dev-server? Yes
? Do you want to simplify the creation of HTML files for your bundle? Yes
? Do you want to add PWA support? No
? Which of the following CSS solutions do you want to use? CSS only
? Will you be using PostCSS in your project? No
? Do you want to extract CSS for every file? Yes
? Do you like to install prettier to format generated configuration? Yes
? Pick a package manager: npm

## reactのインストール
$ npm install react react-dom

webpack serveを起動すると

$ npm run serve

index.htmlに初期で書かれている Hello world! が表示されていることを確認する。

作成した環境でJSXを使ってみる(使えないことの確認)

index.jsを以下に書き換えて、JSXが使えないことを確認する。

import { ReactDOM } from "react"

const Sample = () => {
  return (
    <div>
      <h1>Hello world!</h1>
      <h2>Tip: Check your console</h2>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Sample />)

JSXが使えないことを確認

Compiled with problems:X

ERROR in ./src/index.js 5:4

Module parse failed: Unexpected token (5:4)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| const Sample = () => {
|   return (
>     <>
|       <h1>Hello world!</h1>
|       <h2>Tip: Check your console</h2>

ちなみに index.jsから index.jsx に書き換えて、webpack.config.jsのエントリーポイントを index.jsx にしてもエラーになります。(コンパイルしてないので、、、)

JSXを使わずに表示してみる

index.js を以下に書き換える。

JSXを使わない場合は React.createElement(component, props, …children) を呼び出す必要があります。JSXの要素はこれのシンタックスシュガーなのでJSX使わずにReactでアプリ開発することは可能です。

というわけで、Fragmentを先頭に、childrenに定義したDOMを詰めていきます。

import React from "react"
import ReactDOM from 'react-dom/client'

const Sample = () => {
  return React.createElement(
    React.Fragment, null, 
    React.createElement("h1", null, 
    "Hello world!"), 
    React.createElement("h2", null, 
    "Tip: Check your console"));
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(React.createElement("div", null, Sample()))

すでに苦しいのがわかります。。。(もうJSXが嫌いではなくなりましたか?)

ここまでで事前準備とJSXを使わずにReactによるアプリケーションの表示ができるようになったので、Reactチュートリアルをやっていきましょう!

JSXを使わないReactチュートリアル

スターターコードです。

import React from "react"
import ReactDOM from 'react-dom/client'
import "../index.css"

const Square = () => {
  return React.createElement("button", {
    className: "square"
  });
}
const Board = () => {
  const renderSquare = (i) => {
    return React.createElement(Square, null);
  }

  const status = 'Next player: X';
  return React.createElement("div", null, React.createElement("div", {
    className: "status"
  }, status), React.createElement("div", {
    className: "board-row"
  }, renderSquare(0), renderSquare(1), renderSquare(2)), React.createElement("div", {
    className: "board-row"
  }, renderSquare(3), renderSquare(4), renderSquare(5)), React.createElement("div", {
    className: "board-row"
  }, renderSquare(6), renderSquare(7), renderSquare(8)));
}

const Game = () => {
  return React.createElement("div", {
    className: "game"
  }, React.createElement("div", {
    className: "game-board"
  }, React.createElement(Board, null)), React.createElement("div", {
    className: "game-info"
  }, React.createElement("div", null), React.createElement("ol", null)));
}

// ========================================

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render( React.createElement(Game, null));

あれ、意外といける?と思いましたが 全く階層がわからない笑

数字表示

propsを渡して、数字を表示します。

-const Square = () => {
+const Square = (props) => {
   return React.createElement("button", {
     className: "square"
-  });
+  }, props.value);
 }
 const Board = () => {
   const renderSquare = (i) => {
-    return React.createElement(Square, null);
+    return React.createElement(Square, { value: i });
   }
 
   const status = 'Next player: X';

onClickイベントでXを表示させる

-import React from "react"
+import React, { useState } from "react"
 import ReactDOM from 'react-dom/client'
 import "../index.css"
 
-const Square = (props) => {
+const Square = () => {
+  const [value, setValue] = useState(null);
+
   return React.createElement("button", {
-    className: "square"
-  }, props.value);
+    className: "square",
+    onClick: () => setValue('X'),
+  }, value);
 }
 const Board = () => {
   const renderSquare = (i) => {

propsにonClickイベントが入ってくるようになって、さらに見づらくなってきました。。。

Stateのリフトアップ

SquareコンポーネントからBoardコンポーネントへState情報を移し、親でStateを管理させるようにします。

-const Square = () => {
-  const [value, setValue] = useState(null);
-
+const Square = (props) => {
   return React.createElement("button", {
     className: "square",
-    onClick: () => setValue('X'),
-  }, value);
+    onClick: () => props.onClick()
+  }, props.value);
 }
 const Board = () => {
+  const [squares, setSquares] = useState(Array(9).fill(null))
+
+  const handleClick = (i) => {
+    const newSquares = squares.slice();
+    newSquares[i] = "X";
+    setSquares(newSquares)
+  }
+
   const renderSquare = (i) => {
-    return React.createElement(Square, { value: i });
+    return React.createElement(Square, { 
+      value: squares[i],
+      onClick: () => handleClick(i)
+    });
   }
 
   const status = 'Next player: X';

ここはDOM に関わる部分の変更がほぼないので、そんなに影響はないですね。

ゲームの勝敗判定

const Board = () => {
   const [squares, setSquares] = useState(Array(9).fill(null))
+  const [xIsNext, setXIsNext] = useState(true)
 
   const handleClick = (i) => {
     const newSquares = squares.slice();
-    newSquares[i] = "X";
+    if (calculateWinner(newSquares) || newSquares[i]) {
+      return;
+    }
+    newSquares[i] = xIsNext ? "X" : "O"
     setSquares(newSquares)
+    setXIsNext(!xIsNext)
   }
-  const status = 'Next player: X';
+  const winner = calculateWinner(squares);
+  let status;
+  if (winner) {
+    status = 'Winner: ' + winner;
+  } else {
+    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
+  }
+
   return React.createElement("div", null, React.createElement("div", {
     className: "status"
   }, status), React.createElement("div", {
const root = ReactDOM.createRoot(document.getElementById("root"));
 root.render( React.createElement(Game, null));
+
+
+const calculateWinner = (squares) => {
+  const lines = [
+    [0, 1, 2],
+    [3, 4, 5],
+    [6, 7, 8],
+    [0, 3, 6],
+    [1, 4, 7],
+    [2, 5, 8],
+    [0, 4, 8],
+    [2, 4, 6],
+  ];
+  for (let i = 0; i < lines.length; i++) {
+    const [a, b, c] = lines[i];
+    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
+      return squares[a];
+    }
+  }
+  return null;
+}

タイムトラベル機能(履歴表示)

ここからは、手順を戻すためのタイムトラベル機能を作成していきます。

Stateのリフトアップ等もまとめてやってしまいます。

diff --git a/src/index.js b/src/index.js
index 56cf2ab..b6c8f75 100644
--- a/src/index.js
+++ b/src/index.js
@@ -8,35 +8,14 @@ const Square = (props) => {
     onClick: () => props.onClick()
   }, props.value);
 }
-const Board = () => {
-  const [squares, setSquares] = useState(Array(9).fill(null))
-  const [xIsNext, setXIsNext] = useState(true)
-
-  const handleClick = (i) => {
-    const newSquares = squares.slice();
-    if (calculateWinner(newSquares) || newSquares[i]) {
-      return;
-    }
-    newSquares[i] = xIsNext ? "X" : "O"
-    setSquares(newSquares)
-    setXIsNext(!xIsNext)
-  }
-
+const Board = (props) => {
   const renderSquare = (i) => {
     return React.createElement(Square, { 
-      value: squares[i],
-      onClick: () => handleClick(i)
+      value: props.squares[i],
+      onClick: () => props.onClick(i)
     });
   }
 
-  const winner = calculateWinner(squares);
-  let status;
-  if (winner) {
-    status = 'Winner: ' + winner;
-  } else {
-    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
-  }
-
   return React.createElement("div", null, React.createElement("div", {
     className: "status"
   }, status), React.createElement("div", {
@@ -49,13 +28,57 @@ const Board = () => {
 }
 
 const Game = () => {
+  const [history, setHistory] = useState([{ squares: Array(9).fill(null) }]);
+  const [stepNumber, setStepNumber] = useState(0);
+  const [xIsNext, setXIsNext] = useState(true);
+
+  const handleClick = (i) => {
+    const historySlice = history.slice(0, stepNumber + 1);
+    const current = historySlice[historySlice.length - 1];
+    const squares = current.squares.slice();
+    if (calculateWinner(squares) || squares[i]) {
+      return;
+    }
+    squares[i] = xIsNext ? 'X' : 'O';
+    setHistory(
+      historySlice.concat([
+        {
+          squares: squares,
+        },
+      ])
+    );
+    setStepNumber(historySlice.length);
+    setXIsNext(!xIsNext);
+  };
+
+  const current = history[history.length - 1];
+  const winner = calculateWinner(current.squares);
+  const moves = history.map((step, move) => {
+    const desc = move ? 'Go to move #' + move : 'Go to game start';
+    return React.createElement("li", {
+      key: move
+    },React.createElement("button", {
+      onClick: () => jumpTo(move)
+    }, desc));
+  });
+  let status;
+  if (winner) {
+    status = 'Winner: ' + winner;
+  } else {
+    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
+  }
+
   return React.createElement("div", {
     className: "game"
   }, React.createElement("div", {
     className: "game-board"
-  }, React.createElement(Board, null)), React.createElement("div", {
+  }, React.createElement(Board, {
+    squares: current.squares,
+    onClick: i => handleClick(i)
+  })), React.createElement("div", {
     className: "game-info"
-  }, React.createElement("div", null), React.createElement("ol", null)));
+  }, React.createElement("div", null, status), 
+  React.createElement("ol", null, moves)));
 }

statusやmovesのchildrenを渡す部分に時間がかかったり

-  }, React.createElement("div", null), React.createElement("ol", null)));
+  }, React.createElement("div", null, status), 
+  React.createElement("ol", null, moves)));

レンダリングのkeyに設定するpropsの渡し方や場所が見当たらず、とてもやりづらい。。。

return React.createElement("li", {
+      key: move
+    }

タイムトラベル機能(履歴ジャンプ)

最後に jumpTo を実装して、 current をstepNumberを見るようにしたら完成!

diff --git a/src/index.js b/src/index.js
index b6c8f75..3f72c7a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -51,7 +51,12 @@ const Game = () => {
     setXIsNext(!xIsNext);
   };
 
-  const current = history[history.length - 1];
+  const jumpTo = (step) => {
+    setStepNumber(step);
+    setXIsNext(step % 2 === 0);
+  };
+
+  const current = history[stepNumber];
   const winner = calculateWinner(current.squares);
   const moves = history.map((step, move) => {
     const desc = move ? 'Go to move #' + move : 'Go to game start

最後に

本記事ではJSXを使用せずにReact公式チュートリアルをやってきました。
JSX嫌いな人は calssclassName になったりという理由だと思うのですが、JSXを使わない方が大変な場合があります。あくまでも React.createElement(component, props, …children) のシンタックスシュガーなので、JSXのことが嫌いから普通になってれば幸いです。
例え嫌いでも、Reactは書けるのはわかったのでドンドンReactで開発しましょう!
(このやり方してたら、JSXじゃなくて 自分が嫌われそう。。。)

また、Babelで JSX → JS 試せるので遊んでみてはいかがでしょうか。
https://babeljs.io/repl/#?browsers=defaults%2C not ie 11%2C not ie_mob 11&build=&builtIns=false&corejs=3.21&spec=false&loose=false&code_lz=GYVwdgxgLglg9mABACwKYBt1wBQEpEDeAUIogE6pQhlIA8AJjAG4B8AEhlogO5xnr0AhLQD0jVgG4iAXyJA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=false&fileSize=false&timeTravel=false&sourceType=module&lineWrap=true&presets=react&prettier=false&targets=&version=7.19.4&externalPlugins=&assumptions={}

それでもJSXが嫌いな人へ

違うシンタックスでのReactのマークアップのライブラリもあります!!
https://github.com/mlmorg/react-hyperscript

https://github.com/ohanhi/hyperscript-helpers

https://github.com/pugjs/babel-plugin-transform-react-pug

参考

https://ja.reactjs.org/tutorial/tutorial.html
https://ja.reactjs.org/docs/react-without-jsx.html

Pete Huntさんの素晴らしい発表を見て、JSXについての考えを改めましょう。
https://www.youtube.com/watch?v=x7cQ3mrcKaY

Discussion