ReactとJSX
import React from "react";
const Component = () => <div />;
^
JSX
このJSXはJSに変換するとどうなるんだろう?
BabelはこのプラグインでJSXをJSに変換している
"plugins": ["@babel/plugin-transform-react-jsx"]
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
を指定する
"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する関数
"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"}]
以上です。