132
51

More than 5 years have passed since last update.

JSXのファクトリ関数を自作する方法と、Reactと全然違う挙動をさせるサンプル

Last updated at Posted at 2018-09-21
1 / 36

ReactとJSX


import React from "react";

const Component = () => <div />;
                        ^
                       JSX

このJSXはJSに変換するとどうなるんだろう?


BabelはこのプラグインでJSXをJSに変換している

.babelrc
"plugins": ["@babel/plugin-transform-react-jsx"]

1.js
import React from "react";

const Component = () => <div />;

変換してみます

$ npx babel 1.js
import React from "react";

const Component = () => React.createElement("div", null);

つまりこういう風に変換される

<div />

React.createElement("div", null)

なるほど。
色々なパターンを試してみましょう


属性を付けてみる

<div id="container" />

React.createElement("div", {
  id: "container"
})

第二引数にObjectで渡される


タグの中身を入れてみる

<div id="container">Content</div>

React.createElement("div", {
  id: "container"
}, "Content")

第三引数に文字列で渡される


JSXを入れ子にしてみる

<div id="container">
  <h1>Heading</h1>
</div>

React.createElement("div", {
  id: "container"
}, React.createElement("h1", null, "Heading"))

文字列でなくReact.createElementになる


中身を複数入れてみる

<div id="container">
  <h1>Heading</h1>
  <p>Paragraph</p>
</div>

React.createElement(
  "div",
  {
    id: "container"
  },
  React.createElement("h1", null, "Heading"),
  React.createElement("p", null, "Paragraph")
)

可変長引数だった


{}を使ってみる

<div id={id}>
  <h1>Heading</h1>
  <p>Paragraph</p>
  {id}
</div>

React.createElement(
  "div",
  {
    id: id
  },
  React.createElement("h1", null, "Heading"),
  React.createElement("p", null, "Paragraph"),
  id
)

式がそのまま渡される


作ったコンポーネントを使ってみる

const Child = ({ id }) => (
  <div id={id}>
    <h1>Heading</h1>
    <p>Paragraph</p>
    {id}
  </div>
);

const Parent = () => <Child id="child" />;

const Parent = () => React.createElement(Child, { id: "child" });

キャピタライズされたタグ名だと文字列でなくなる。
だからコンポーネントはクラスでも関数でもキャピタライズするんですね。


他のライブラリとJSX


PreactやHyperappを使うときは、pragmaプロパティにファクトリ関数hを指定する

.babelrc
"plugins": [[
  "@babel/plugin-transform-react-jsx",
  { "pragma": "h" }
]]

そうすると、

<div id="container">Content</div>

h("div", { id: "container" }, "Content")

未設定だとReact.createElementに置き換えられるところが、hに置き換わる


React

React.createElement("div", {
  id: "container"
}, "Content")

Preact / Hyperapp

h("div", {
  id: "container"
}, "Content")

つまり、

  • 第一引数に文字列または何らかのオブジェクト
  • 第二引数にObject
  • 以降可変長引数を受け取る

っていう関数を作って、transform-react-jsxプラグインのpragmaに設定すれば、オリジナルなJSXの挙動を作れる。


⚠ ここから先はグロテスクな表現が含まれています


こんなファクトリ関数を作ってみる

const pragma = (func, _, ...args) =>
  func.apply(null, args);

第一引数に第三引数を渡してapplyする関数

.babelrc
"plugins": [[
  "@babel/plugin-transform-react-jsx",
  { "pragma": "pragma" }
]]

こんなJSXを書いてみる

<console.log>Hello World!</console.log>

変換するとこうなる

pragma(console.log, null, "Hello World!")

pragmaは第一引数に第三引数を渡してapplyする関数なので、こういうことですね

console.log.apply(null, "Hello World!")

つまり実行すると

$ npx babel-node main.js
Hello World!

うまく動きました。
では色々な関数を実行してみます。


<console.log>
  <Math.sqrt>{2}</Math.sqrt>
  <Math.pow>
    {2}
    {4}
  </Math.pow>
  <Math.ceil>
    <Math.random />
  </Math.ceil>
</console.log>;
$ npx babel-node main.js
1.4142135623730951 16 1

面白いですね〜


私の中の悪いスイッチが入ってしまいまして、JSXでWebバックエンドも書きたくなってきました。

const express = require("express");
const app = express();
const port = 3000;

app.get("/", (req, res) => res.send("Hello World!"));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

これを書き換えてみます。


const express = <require>express</require>;
const app = <express />;
const port = 3000;

<app.get>
  /{(req, res) => <res.send>Hello World!</res.send>}
</app.get>;

<app.listen>
  {port}
  {() => <console.log>Example app listening on port {port}!</console.log>}
</app.listen>;

requireタグに戦慄を感じるのは私だけではないはずです

$ npx babel-node 13.js
13.js:1
const pragma = (func, _, ...args) => func.apply(null, args);
                                          ^
TypeError: func.apply is not a function

requireが"require"になってしまうので失敗します。


evalしちゃう。

const pragma = (func, _, ...args) => {
  if (typeof func === "string") {
    func = eval(func);
  }
  return func.apply(null, args);
};

改めて

$ npx babel-node 14.js
node_modules/express/lib/application.js:479
    this.lazyrouter();
         ^
TypeError: Cannot read property 'lazyrouter' of null

func.apply(null, args)としててthisを正しく渡していないので、<app.get>などでエラーがでる


<app.get this={app}>という感じで渡すようにした

const pragma = (func, props, ...args) => {
  if (typeof func === "string") {
    func = eval(func);
  }

  if (props && props.this) {
    return func.apply(props.this, args);
  } else {
    return func.apply(null, args);
  }
};
const express = <require>express</require>;
const app = <express />;
const port = 3000;

<app.get this={app}>
  /{(req, res) => <res.send this={res}>Hello World!</res.send>}
</app.get>;

<app.listen this={app}>
  {port}
  {() => <console.log>Example app listening on port {port}!</console.log>}
</app.listen>;

$ npx babel-node 15.js
Example app listening on port  3000 !

起動した

$ curl http://localhost:3000
Hello World!

ちゃんと動いた
こうなるとエスカレートが止まりません


もっとJSX感がほしい

<express callback={app => (
  <app.get this={app} />
) />

とできるようにファクトリを変更する

const pragma = (func, props, ...args) => {
  if (typeof func === "string") {
    func = eval(func);
  }

  let returnValue;
  if (props && props.this) {
    returnValue = func.apply(props.this, args);
  } else {
    returnValue = func.apply(null, args);
  }

  if (props && props.callback) {
    props.callback(returnValue);
  }

  return returnValue;
};

溢れ出すJSX感

const port = 3000;

<require
  callback={Express => (
    <Express
      callback={app => (
        <>
          <app.get this={app}>
            /{(req, res) => <res.send this={res}>Hello World!</res.send>}
          </app.get>
          <app.listen this={app}>
            {port}
            {() => (
              <console.log>Example app listening on port {port}!</console.log>
            )}
          </app.listen>
        </>
      )}
    />
  )}
>
  express
</require>;

コンポーネントぽく整理するとよりJSX感UP!

const HelloHandler = () => (req, res) => (
  <res.send this={res}>Hello World!</res.send>
);

const ListenHandler = port => () => (
  <console.log>Example app listening on port {port}!</console.log>
);

const Config = port => app => (
  <>
    <app.get this={app}>
      /<HelloHandler />
    </app.get>
    <app.listen this={app}>
      {port}
      <ListenHandler>{port}</ListenHandler>
    </app.listen>
  </>
);

const App = port => Express => <Express callback={Config(port)} />;

<require callback={App(port)}>express</require>;

$ npx babel-node 17.js
Example app listening on port  3000 !
$ curl http://localhost:3000
Hello World!

動きますね
気を良くしたのでDBもつないじゃいます


元のDB処理サンプル

const sqlite3 = require("sqlite3").verbose();

let db = new sqlite3.Database("./app.db", err => {
  if (err) {
    console.error(err.message);
  }
  console.log("Connected to the database.");
});

db.serialize(() => {
  db.run(
    `CREATE TABLE IF NOT EXISTS members (
      id integer PRIMARY KEY AUTOINCREMENT,
      name text NOT NULL
    )`,
    err => err && console.error(err.message)
  );

  db.run(`INSERT INTO members (name) VALUES ("Yamamoto")`, function(err) {
    if (err) {
      return console.error(err.message);
    }
    console.log(this.lastID);
  });

  db.all("SELECT * FROM members", (err, rows) => {
    if (err) {
      return console.error(err.message);
    }
    console.log(rows);
  });
});

db.close(err => {
  if (err) {
    console.error(err.message);
  }
  console.log("Close the database connection.");
});

newが出てきた。こういう感じで仕上げたい

<sqlite3.Database new callback={Process}>
  ./app.db
  <DoneHandler>Connected to the database.</DoneHandler>
</sqlite3.Database>

new属性に対応する

const pragma = (func, props, ...args) => {
  if (typeof func === "string") {
    func = eval(func);
  }

  let returnValue;
  if (props && props.this) {
    returnValue = func.apply(props.this, args);
  } else if (props && props.new) {
    returnValue = new (Function.prototype.bind.apply(func, [null, ...args]))();
  } else {
    returnValue = func.apply(null, args);
  }

  if (props && props.callback) {
    props.callback(returnValue);
  }

  return returnValue;
};

こんな感じになりました。
Markup LanguageとQuery Languageの絶妙な融合に酔いしれましょう。

const Process = db => (
  <>
    <db.serialize this={db}>
      {() => (
        <>
          <db.run this={db}>
            CREATE TABLE IF NOT EXISTS members ( id integer PRIMARY KEY
            AUTOINCREMENT, name text NOT NULL )
            <DoneHandler>Created the table.</DoneHandler>
          </db.run>

          <db.run this={db}>
            INSERT INTO members (name) VALUES ("Yamamoto")
            <InsertHandler />
          </db.run>

          <db.all this={db}>
            SELECT * FROM members
            <AllHandler />
          </db.all>
        </>
      )}
    </db.serialize>

    <db.close this={db}>
      <DoneHandler>Close the database connection.</DoneHandler>
    </db.close>
  </>
);

const App = sqlite3 => (
  <sqlite3.Database new callback={Process}>
    ./app.db
    <DoneHandler>Connected to the database.</DoneHandler>
  </sqlite3.Database>
);

<require
  callback={sqlite3 => <sqlite3.verbose this={sqlite3} callback={App} />}
>
  sqlite3
</require>;

const DoneHandler = msg => err =>
  err ? (
    <console.error>{err.message}</console.error>
  ) : (
    <console.log>{msg}</console.log>
  );

const InsertHandler = () =>
  function(err) {
    return err ? (
      <console.error>{err.message}</console.error>
    ) : (
      <console.log>{this.lastID}</console.log>
    );
  };

const AllHandler = () => (err, rows) =>
  err ? (
    <console.error>{err.message}</console.error>
  ) : (
    <console.log>{rows}</console.log>
  );

そしてCRUDのREST APIにDB処理を組み込む

const Config = (port, bodyParser, Cors, sqlite3) => app => (
  <>
    <app.use this={app}>
      <Cors />
    </app.use>
    <app.use this={app}>
      <bodyParser.json />
    </app.use>
    <app.get this={app}>
      /<HelloHandler />
    </app.get>
    <app.post this={app}>
      /members
      <CreateMemberHandler>{sqlite3}</CreateMemberHandler>
    </app.post>
    <app.get this={app}>
      /members
      <ListMembersHandler>{sqlite3}</ListMembersHandler>
    </app.get>
    <app.get this={app}>
      /members/:id
      <RetrieveMemberHandler>{sqlite3}</RetrieveMemberHandler>
    </app.get>
    <app.put this={app}>
      /members/:id
      <UpdateMemberHandler>{sqlite3}</UpdateMemberHandler>
    </app.put>
    <app.delete this={app}>
      /members/:id
      <DeleteMemberHandler>{sqlite3}</DeleteMemberHandler>
    </app.delete>
    <app.listen this={app}>
      {port}
      <ListenHandler>{port}</ListenHandler>
    </app.listen>
  </>
);

const App = (port, bodyParser, cors, Express, sqlite3) => (
  <>
    <QueryDatabase>
      {sqlite3}
      {db => (
        <db.run this={db}>
          CREATE TABLE IF NOT EXISTS members ( id integer PRIMARY KEY
          AUTOINCREMENT, name text NOT NULL )
          <DoneHandler>Created the table.</DoneHandler>
        </db.run>
      )}
    </QueryDatabase>
    <Express callback={Config(port, bodyParser, cors, sqlite3)} />
  </>
);

const HelloHandler = () => (req, res) => (
  <res.send this={res}>Hello World!</res.send>
);

const DoneHandler = msg => err =>
  err ? (
    <console.error>{err.message}</console.error>
  ) : (
    <console.log>{msg}</console.log>
  );

const QueryDatabase = (sqlite3, process) => (
  <sqlite3.Database
    new
    callback={db => (
      <>
        {process(db)}
        <db.close this={db}>
          <DoneHandler>Close the database connection.</DoneHandler>
        </db.close>
      </>
    )}
  >
    ./app.db
    <DoneHandler>Connected to the database.</DoneHandler>
  </sqlite3.Database>
);

const CreateMemberResponse = (req, res) =>
  function(err) {
    err ? (
      <>
        <console.error>{err.message}</console.error>
        <res.status
          this={res}
          callback={res => <res.send this={res}>{err.message}</res.send>}
        >
          {500}
        </res.status>
      </>
    ) : (
      <>
        <console.log>{this.lastID}</console.log>
        <res.json this={res}>
          {{ id: this.lastID, name: req.body.name }}
        </res.json>
      </>
    );
  };

const CreateMemberHandler = sqlite3 => (req, res) => (
  <QueryDatabase>
    {sqlite3}
    {db => (
      <db.run this={db}>
        INSERT INTO members(name) VALUES(?)
        {[req.body.name]}
        <CreateMemberResponse>
          {req}
          {res}
        </CreateMemberResponse>
      </db.run>
    )}
  </QueryDatabase>
);

const ListMembersResponse = res => (err, rows) =>
  err ? (
    <>
      <console.error>{err.message}</console.error>
      <res.status
        this={res}
        callback={res => <res.send this={res}>{err.message}</res.send>}
      >
        {500}
      </res.status>
    </>
  ) : (
    <res.json this={res}>{rows}</res.json>
  );

const ListMembersHandler = sqlite3 => (req, res) => (
  <QueryDatabase>
    {sqlite3}
    {db => (
      <db.all this={db}>
        SELECT * FROM members
        <ListMembersResponse>{res}</ListMembersResponse>
      </db.all>
    )}
  </QueryDatabase>
);

const RetrieveMemberResponse = res => (err, row) =>
  err ? (
    <>
      <console.error>{err.message}</console.error>
      <res.status
        this={res}
        callback={res => <res.send this={res}>{err.message}</res.send>}
      >
        {500}
      </res.status>
    </>
  ) : (
    <res.json this={res}>{row}</res.json>
  );

const RetrieveMemberHandler = sqlite3 => (req, res) => (
  <QueryDatabase>
    {sqlite3}
    {db => (
      <db.get this={db}>
        SELECT * FROM members WHERE id = ?{[req.params.id]}
        <ListMembersResponse>{res}</ListMembersResponse>
      </db.get>
    )}
  </QueryDatabase>
);

const UpdateMemberResponse = (req, res) =>
  function(err) {
    err ? (
      <>
        <console.error>{err.message}</console.error>
        <res.status
          this={res}
          callback={res => <res.send this={res}>{err.message}</res.send>}
        >
          {500}
        </res.status>
      </>
    ) : (
      <>
        <console.log>{this.changes}</console.log>
        <res.json this={res}>
          {{ id: req.params.id, name: req.body.name }}
        </res.json>
      </>
    );
  };

const UpdateMemberHandler = sqlite3 => (req, res) => (
  <QueryDatabase>
    {sqlite3}
    {db => (
      <db.run this={db}>
        UPDATE members SET name = ? WHERE id = ?{[req.body.name, req.params.id]}
        <UpdateMemberResponse>
          {req}
          {res}
        </UpdateMemberResponse>
      </db.run>
    )}
  </QueryDatabase>
);

const DeleteMemberResponse = (req, res) =>
  function(err) {
    err ? (
      <>
        <console.error>{err.message}</console.error>
        <res.status
          this={res}
          callback={res => <res.send this={res}>{err.message}</res.send>}
        >
          {500}
        </res.status>
      </>
    ) : (
      <>
        <console.log>{this.changes}</console.log>
        <res.status this={res} callback={res => <res.send this={res} />}>
          {204}
        </res.status>
      </>
    );
  };

const DeleteMemberHandler = sqlite3 => (req, res) => (
  <QueryDatabase>
    {sqlite3}
    {db => (
      <db.run this={db}>
        DELETE from members WHERE id = ?{[req.params.id]}
        <DeleteMemberResponse>
          {req}
          {res}
        </DeleteMemberResponse>
      </db.run>
    )}
  </QueryDatabase>
);

const ListenHandler = port => () => (
  <console.log>Example app listening on port {port}!</console.log>

// 美しくない!!
<require
  callback={bodyParser => (
    <require
      callback={cors => (
        <require
          callback={express => (
            <require
              callback={sqlite3 => (
                <sqlite3.verbose
                  this={sqlite3}
                  callback={sqlite3 =>
                    App(port, bodyParser, cors, express, sqlite3)
                  }
                />
              )}
            >
              sqlite3
            </require>
          )}
        >
          express
        </require>
      )}
    >
      cors
    </require>
  )}
>
  body-parser
</require>;

$ npx babel-node 21.js
Example app listening on port  3000 !

見事動きました

$ curl http://localhost:3000/members
[{"id":1,"name":"Yamamoto"}]

$ curl http://localhost:3000/members/1
{"id":1,"name":"Yamamoto"}

$ curl http://localhost:3000/members -X POST -H "Content-Type: application/json" -d '{"name": "Takahashi"}'
{"id":2,"name":"Takahashi"}

$ curl http://localhost:3000/members
[{"id":1,"name":"Yamamoto"},{"id":2,"name":"Takahashi"}]

$ curl http://localhost:3000/members/1 -X PUT -H "Content-Type: application/json" -d '{"name": "Suzuki"}'
{"id":"1","name":"Suzuki"}

$ curl http://localhost:3000/members
[{"id":1,"name":"Suzuki"},{"id":2,"name":"Takahashi"}]

$ curl http://localhost:3000/members/1 -X DELETE

$ curl http://localhost:3000/members
[{"id":2,"name":"Takahashi"}]

以上です。

コードはこちらです。
https://github.com/boiyaa/dont-use-jsx-like-this

132
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
132
51