20190307のReactに関する記事は10件です。

Reactのチュートリアル

最終目標

フロントエンドとバックエンドのDB排他を調べてみたい。

今回の目標

Tutorial: Intro to Reactを実施する
https://reactjs.org/tutorial/tutorial.html
https://mae.chab.in/archives/2943

環境

Ubuntu 18.04.2 LTS
node v8.10.0
npm 3.5.2
create-react-app v2.1.5

詳細は、https://qiita.com/y_ohr/items/8afd0852b24356059f2b 参照。

Tutorial: Intro to React

ローカルでやります。

react-app作成

$ create-react-app tutorial-react-app
$ cd tutorial-react-app/

src配下削除

$ cd src/
$ rm -f *
$ cd ..

src/index.css作成

src/index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

src/index.js作成

src/index.js
class Square extends React.Component {
    render() {
        return (
            <button className="square">
                {/* TODO */}
            </button>
        );
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return <Square />;
    }

    render() {
        const status = 'Next player: X';

        return (
            <div>
                <div className="status">{status}</div>
                <div className="board-row">
                    {this.renderSquare(0)}
                    {this.renderSquare(1)}
                    {this.renderSquare(2)}
                </div>
                <div className="board-row">
                    {this.renderSquare(3)}
                    {this.renderSquare(4)}
                    {this.renderSquare(5)}
                </div>
                <div className="board-row">
                    {this.renderSquare(6)}
                    {this.renderSquare(7)}
                    {this.renderSquare(8)}
                </div>
            </div>
        );
    }
}

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="game-board">
                    <Board />
                </div>
                <div className="game-info">
                    <div>{/* status */}</div>
                    <ol>{/* TODO */}</ol>
                </div>
            </div>
        );
    }
}

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

ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

index.jsの先頭に3行追加。ここからはdiffにて。

src/index.js
diff --git a/src/index.js b/src/index.js
index 9a06c07..70f7c67 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,3 +1,7 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+
 class Square extends React.Component {
     render() {
         return (

Passing Data Through Props

src/index.js
diff --git a/src/index.js b/src/index.js
index 70f7c67..43522c3 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,7 +6,7 @@ class Square extends React.Component {
     render() {
         return (
             <button className="square">
-                {/* TODO */}
+                {this.props.value}
             </button>
         );
     }
@@ -14,7 +14,7 @@ class Square extends React.Component {

 class Board extends React.Component {
     renderSquare(i) {
-        return <Square />;
+        return <Square value={i} />;
     }

     render() {

Making an Interactive Component

src/index.js
diff --git a/src/index.js b/src/index.js
index 43522c3..b908669 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,10 +3,19 @@ import ReactDOM from 'react-dom';
 import './index.css';

 class Square extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            value: null,
+        };
+    }
+
     render() {
         return (
-            <button className="square">
-                {this.props.value}
+            <button
+                className="square"
+                onClick={() => this.setState({ value: 'X' })}>
+                {this.state.value}
             </button>
         );
     }

Lifting State Up

src/index.js
diff --git a/src/index.js b/src/index.js
index b908669..01c54da 100644
--- a/src/index.js
+++ b/src/index.js
@@ -3,27 +3,38 @@ import ReactDOM from 'react-dom';
 import './index.css';

 class Square extends React.Component {
-    constructor(props) {
-        super(props);
-        this.state = {
-            value: null,
-        };
-    }
-
     render() {
         return (
             <button
                 className="square"
-                onClick={() => this.setState({ value: 'X' })}>
-                {this.state.value}
+                onClick={() => this.props.onClick()}>
+                {this.props.value}
             </button>
         );
     }
 }

 class Board extends React.Component {
+    constructor() {
+        super();
+        this.state = {
+            squares: Array(9).fill(null),
+        };
+    }
+
+    handleClick(i) {
+        const squares = this.state.squares.slice();
+        squares[i] = 'X';
+        this.setState({ squares: squares });
+    }
+
     renderSquare(i) {
-        return <Square value={i} />;
+        return (
+            <Square
+                value={this.state.squares[i]}
+                onClick={() => this.handleClick(i)}
+            />
+        );
     }

     render() {

Function Components

src/index.js
diff --git a/src/index.js b/src/index.js
index 01c54da..5f16f3d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,16 +2,14 @@ import React from 'react';
 import ReactDOM from 'react-dom';
 import './index.css';

-class Square extends React.Component {
-    render() {
-        return (
-            <button
-                className="square"
-                onClick={() => this.props.onClick()}>
-                {this.props.value}
-            </button>
-        );
-    }
+function Square(props) {
+    return (
+        <button
+            className="square"
+            onClick={props.onClick}>
+            {props.value}
+        </button>
+    );
 }

 class Board extends React.Component {

Taking Turns

src/index.js
diff --git a/src/index.js b/src/index.js
index 5f16f3d..fa7e58a 100644
--- a/src/index.js
+++ b/src/index.js
@@ -17,13 +17,17 @@ class Board extends React.Component {
         super();
         this.state = {
             squares: Array(9).fill(null),
+            xIsNext: true,
         };
     }

     handleClick(i) {
         const squares = this.state.squares.slice();
-        squares[i] = 'X';
-        this.setState({ squares: squares });
+        squares[i] = this.state.xIsNext ? 'X' : 'O';
+        this.setState({
+            squares: squares,
+            xIsNext: !this.state.xIsNext,
+        });
     }

     renderSquare(i) {
@@ -36,7 +40,7 @@ class Board extends React.Component {
     }

     render() {
-        const status = 'Next player: X';
+        const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

         return (
             <div>

Declaring a Winner

src/index.js
diff --git a/src/index.js b/src/index.js
index fa7e58a..34c275d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -23,6 +23,10 @@ class Board extends React.Component {

     handleClick(i) {
         const squares = this.state.squares.slice();
+        if(calculateWinner(squares) || squares[i]){
+            return;
+        }
+
         squares[i] = this.state.xIsNext ? 'X' : 'O';
         this.setState({
             squares: squares,
@@ -40,7 +44,13 @@ class Board extends React.Component {
     }

     render() {
-        const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+        const winner = calculateWinner(this.state.squares);
+        let status;
+        if (winner) {
+            status = 'Winner: ' + winner;
+        } else {
+            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+        }

         return (
             <div>
@@ -87,3 +97,23 @@ ReactDOM.render(
     <Game />,
     document.getElementById('root')
 );
+
+function 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;
+}

Lifting State Up, Again

src/index.js
diff --git a/src/index.js b/src/index.js
index 34c275d..65a0010 100644
--- a/src/index.js
+++ b/src/index.js
@@ -13,48 +13,18 @@ function Square(props) {
 }

 class Board extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            squares: Array(9).fill(null),
-            xIsNext: true,
-        };
-    }
-
-    handleClick(i) {
-        const squares = this.state.squares.slice();
-        if(calculateWinner(squares) || squares[i]){
-            return;
-        }
-
-        squares[i] = this.state.xIsNext ? 'X' : 'O';
-        this.setState({
-            squares: squares,
-            xIsNext: !this.state.xIsNext,
-        });
-    }
-
     renderSquare(i) {
         return (
             <Square
-                value={this.state.squares[i]}
-                onClick={() => this.handleClick(i)}
+                value={this.props.squares[i]}
+                onClick={() => this.props.onClick(i)}
             />
         );
     }

     render() {
-        const winner = calculateWinner(this.state.squares);
-        let status;
-        if (winner) {
-            status = 'Winner: ' + winner;
-        } else {
-            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
-        }
-
         return (
             <div>
-                <div className="status">{status}</div>
                 <div className="board-row">
                     {this.renderSquare(0)}
                     {this.renderSquare(1)}
@@ -76,14 +46,56 @@ class Board extends React.Component {
 }

 class Game extends React.Component {
+    constructor(props) {
+        super(props);
+        this.state = {
+            history: [{
+                squares: Array(9).fill(null),
+            }],
+            xIsNext: true,
+        };
+    }
+
+    handleClick(i) {
+        const history = this.state.history;
+        const current = history[history.length - 1];
+        const squares = current.squares.slice();
+        if (calculateWinner(squares) || squares[i]) {
+            return;
+        }
+
+        squares[i] = this.state.xIsNext ? 'X' : 'O';
+        this.setState({
+            history: history.concat([{
+                squares: squares,
+            }]),
+            xIsNext: !this.state.xIsNext,
+        });
+    }
+
     render() {
+        const history = this.state.history;
+        const current = history[history.length - 1];
+        const winner = calculateWinner(current.squares);
+
+        let status;
+        if (winner) {
+            status = 'Winner: ' + winner;
+        } else {
+            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
+        }
+
         return (
             <div className="game">
                 <div className="game-board">
-                    <Board />
+                    <Board
+                        squares={current.squares}
+                        onClick={(i) => this.handleClick(i)}
+                    />
+
                 </div>
                 <div className="game-info">
-                    <div>{/* status */}</div>
+                    <div>{status}</div>
                     <ol>{/* TODO */}</ol>
                 </div>
             </div>

Showing the Past Moves

src/index.js
diff --git a/src/index.js b/src/index.js
index 65a0010..5afcc7d 100644
--- a/src/index.js
+++ b/src/index.js
@@ -78,6 +78,15 @@ class Game extends React.Component {
         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 (
+                <li>
+                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
+                </li>
+            );
+        });
+
         let status;
         if (winner) {
             status = 'Winner: ' + winner;
@@ -96,7 +105,7 @@ class Game extends React.Component {
                 </div>
                 <div className="game-info">
                     <div>{status}</div>
-                    <ol>{/* TODO */}</ol>
+                    <ol>{moves}</ol>
                 </div>
             </div>
         );

Implementing Time Travel

src/index.js
diff --git a/src/index.js b/src/index.js
index 5afcc7d..0412658 100644
--- a/src/index.js
+++ b/src/index.js
@@ -52,12 +52,13 @@ class Game extends React.Component {
             history: [{
                 squares: Array(9).fill(null),
             }],
+            stepNumber: 0,
             xIsNext: true,
         };
     }

     handleClick(i) {
-        const history = this.state.history;
+        const history = this.state.history.slice(0, this.state.stepNumber + 1);
         const current = history[history.length - 1];
         const squares = current.squares.slice();
         if (calculateWinner(squares) || squares[i]) {
@@ -69,19 +70,27 @@ class Game extends React.Component {
             history: history.concat([{
                 squares: squares,
             }]),
+            stepNumber: history.length,
             xIsNext: !this.state.xIsNext,
         });
     }

+    jumpTo(step) {
+        this.setState({
+            stepNumber: step,
+            xIsNext: (step % 2) === 0,
+        });
+    }
+
     render() {
         const history = this.state.history;
-        const current = history[history.length - 1];
+        const current = history[this.state.stepNumber];
         const winner = calculateWinner(current.squares);

         const moves = history.map((step, move) => {
             const desc = move ? 'Go to move #' + move : 'Go to game start';
             return (
-                <li>
+                <li key={move}>
                     <button onClick={() => this.jumpTo(move)}>{desc}</button>
                 </li>
             );

感想

  • 写経
  • 思ったより時間がかかった=充実したチュートリアルだった。
  • inputで動的に仮想DOMが書き換わるようなプログラムを書いてみたい

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

誰でもわかりやすいGitのコミットメッセージが書けるジェネレーターを作りました

前置き

個人開発でもチーム開発でも、Gitは開発者にとって、ほぼ必要不可欠なツールです。

僕も当然Gitを使っていますが、コミット時のメッセージを書くときにどう書いていいかわからなくて、毎回頭を抱えてしまいます。

そこでジェネレーターを作ることにしました。

出来上がったもの

Git Commenter

git-commenter.gif

Gitのコミットメッセージについて

僕が調べたところGitのコミットメッセージには、統一された書き方は存在しませんでしたが

メッセージ自体の可読性、コミット内容のわかりやすさや、Gitの仕様を考慮して

コミット内容の要約 // 英語では V + O (+M) と書くのが一般的

コミット内容の具体的な理由

という書き方が一般的でした。

また、ここ最近ではコミット内容の把握を簡単にしたりlogやGithubのレポジトリがオシャレになるという理由で、コミット内容のカテゴリを表現する絵文字をコミット内容の要約にprefixとしてつけるのが流行っているようです。(Qiitaにもいくつか絵文字コミットについて言及している記事があります)

このジェネレーターでは、絵文字もサポートして

絵文字 : コミット内容の要約

コミット内容の具体的な理由

という形式のコミットメッセージを生成するようにしました。(もちろん絵文字もない、コミット内容の具体的な理由もない要約だけのコミットメッセージもここでは作れます)

要約については、Githubのファイル欄に表示されたりするためここは日本語英語が入り混じっていると汚いなと思ったので英語統一にしました

コミット内容の具体的な理由については英語が望ましいですが、

  • 具体的な理由を書く必要がある (要約より、英語力が必要であり内容が様々であるためテンプレートもあまり機能しない)
  • Githubのファイル欄や、git log --onelineコマンドで表示されない

以上の理由から日本語でも問題ないのかなと思います。

具体的な使い方

基本的には上記のgifのように実際に使っていただければわかると思います。

まずフォームについて簡単に説明すると

  • Emoji: コミットカテゴリを絵文字で表したもの
  • Verb: コミットの具体的な操作内容 (例: 何かを追加した => Add、何かを更新した => Update)
  • Adjective: コミットの対象を修飾するもの (例: 不必要な => unneeded)
  • Object: コミットの対象 (例: README.mdを更新した => (Update) README.md)
  • Modifier: コミット文を修飾するもの (例: sample.txtをAからBに移動した => (Move sample.txt) from A to B)
  • Reason: コミット内容の具体的な理由
  • Git Comment: 入力から生成されたコミットメッセージが入ります

テンプレート

各フォームの左のセレクトボックスで「テンプレ」を選択すると、コミットメッセージでよく使われる英単語のテンプレート一覧、およびその日本語訳が一覧で出てきます。

テンプレの英単語は簡単なものが多く、コミットメッセージ中でよく使われているため、テンプレの英単語でコミット内容を表せる場合は、これらを使ったメッセージのほうが読みやすいメッセージになると思います。参考にしたQiita記事

また、デフォルトのテンプレート以外に個人がよく使う単語やフレーズは、ユーザーテンプレートとして登録することができます。

登録した単語やフレーズはフォームのテンプレ一覧に表示されるようになります。

git-commenter-template.gif

開発

このWebアプリ自体は、react(+redux)を使って開発しました。 クライアント制作ではよく使うので特に苦労することなくすんなりできました。
(しいて言えば、クリップボードへのコピーがReactの仮想DOMだと難しいです。結局専用のライブラリを使いました。)

当初はWebアプリにするつもりはなかったのですが、必ずGUIを持たせたいとは考えていました。

ただWebアプリ開発以外でGUI開発の経験が特になく、Webアプリでも特に問題はないのでWebアプリにしました。

デプロイに関しては、いつもGithub Pagesにホスティングしていたのですが、勉強もかねて今回はS3にデプロイしました。(しかもRoute53で独自ドメインまで無駄に取りました。)

補足

このWebアプリ開発の前準備のときに調べていて初めて知ったのですが、Gitにはtemplate機能があります。(こちらのQiita記事とかに詳しく載っています)

ですが、僕が普段使っているVSCodeのGitツールではこのテンプレート機能が利用できず、ターミナルをぽちぽちしたり、自分でわかりづらいコミットメッセージを考えたりしなくてはいけなかったため、今回このようなWebアプリをつくりました。

感想

しっかりGoogleで調べましたがGitについてはあまり理解しているとはいえないので、間違っているところやこの機能使えばもっと便利でいいよってところがあったら教えてくださると助かります。

利用した感想とかもあると嬉しいです。

追記(2019/03/11)

予想以上の高評価にびっくりしました。

皆さん本当にありがとうございます。

ユーザーさんから要望のあったコミットメッセージの文字数をチェックする機能を導入しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Hooksでたどる、stateからreducerまで

はじめてReduxの流れを見たときに、複雑さで戸惑いましたが、Reactの状態遷移でいろいろやっていった結果、より基礎的なところから組み立てたほうが、Reducerという世界観を理解できる気がしてきました。

なお、後述の事情により、React Hooksで進めています。また、説明のために必要な箇所を除いて、useCallbackは省略します。

useStateの基本

シンプルな値の格納

React.useStateを使えば、stateとそれを設定する関数を得ることができます。

const [state, setState] = React.useState(0);

<button type="button" onClick={() => setState(1)}>1をセット</button>

「値の設定」と「取得」という、状態を記憶させる上で基本となる機能です。

前の値を使った更新

将来のReactでは非同期レンダリングが入るとのことで、外側にあるstateを読んでsetStateをかけるという方法は推奨されていません。代わりに、setState前の値 => 次の値という形の関数を渡すことで、前の値を使っての更新が可能です。

const [state, setState] = React.useState(0);

<button type="button" onClick={() => setState(x => x + 1)}>インクリメント</button>

オブジェクトをstateにする

useStateの基本機能は上述のとおりですが、stateとしていくつもキーがあるオブジェクトを入れて、更新に際しては一部だけ動かす、というようにする場合、関数による更新が必須となります(そうしないと残りのキーが吹き飛んでしまいます)。

const [state, setState] = React.useState({text: "hoge", count: 1});

const onChangeText = e => {
  const text = e.target.value;
  setState(oldState => ({...oldState, text}));
}

<input type="text" value={state.text} onChange={ onChangeText } />

dispatch関数の抽出

上のonChangeTextでは「更新する値の作成」と「更新の反映」が一体となっていますが、これを切り離すことも可能です。

const [state, setState] = React.useState({text: "hoge", count: 1});

const dispatch = React.useCallback(
  updates => setState(oldState => ({...oldState, ...updates})),
  [setState]
);

const onChangeText = e => {
  const text = e.target.value;
  dispatch({text});
}

<input type="text" value={state.text} onChange={ onChangeText } />

React.useCallbackは、「引数として与えられた関数オブジェクトを、2つ目の引数の配列の値が同じである限りキャッシュする」という機能ですが、これを見ればdispatchsetStateにしか依存していないことがわかるかと思います。

そして、このdispatchは、そのまま「stateの中で更新したいデータを入れれば、その更新を行う」という関数として、stateを引き回さなくても使えるようになります。Contextに入れて下位コンポーネントまで流すのも便利です。

じつは、クラスコンポーネントのthis.setStateは、標準で「オブジェクトのマージ」を行う、これぐらい複雑な機能(関数更新もあるのでもっと複雑か)を持ったメソッドなのでした。よりシンプルを期すために、useStateから始めていました。

reducerの抽出

さらに、「従来のオブジェクトと更新内容から新しいオブジェクトを作る」という部分も、これ単体で抽出が可能です。

// コンポーネントの外に置いても問題なし
const reducer = (oldState, action) => ({...oldState, ...action});

const [state, setState] = React.useState({text: "hoge", count: 1});

const dispatch = React.useCallback(
  updates => setState(oldState => reducer(oldState, updates)),
  [setState]
);

const onChangeText = e => {
  const text = e.target.value;
  dispatch({text});
}

<input type="text" value={state.text} onChange={ onChangeText } />

これだけ見れば、reducer関数を切り出すのは過剰にも思えます。ただ、このように分離することで、reducer部分は「引数のみに依存する、純粋な関数」として構築できるようになり、コンポーネントの状態管理など無関係に、データの更新へ専念できるようになります。

そして、actionも単にマージするだけではなく、例えば動作内容をaction.typeで切り分けるというような、Reducer内で複雑な値の操作を行う方向へ発展させることもできます。

一方で、reducerからdispatchを作るのは定型化していますので、React.useReducerというHooksが用意してあります。

const [state, dispatch] = React.useReducer(reducer, {text: "hoge", count: 1});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React+TypeScript】Functional Componentの定義方法

React+TypeScript.png

React Hooksが来てから、ほとんどFunctional Componentを使うようになりました。

コーディングするときに、デフォルト引数指定やchildren指定はどうやるんだっけと、引っかかることがあったので備忘録として残すことにします。


まず、ヘッダ部分とプロパティの定義部分の例です。
プロパティの省略が出来ないPropsと、省略可能のプロパティがあるPropsOptionalを定義しています。

import React from "react";
import { storiesOf } from "@storybook/react";

const s = storiesOf("Functional Component", module);

// 省略不可
type Props = {
  x: number;
  y: number;
};

// 省略可能のプロパティあり
type PropsOptional = {
  x?: number;
  y: number;
};

コンポーネントの使用例の部分はStorybookを使ってます。
https://qiita.com/otanu/items/dd06d6702f673268c74e

引数をpropsで定義

基本パターン。

const Ex = (props: Props) => {
  return (
    <div>
      {props.x}:{props.y}
    </div>
  );
};
s.add("Ex", () => <Ex x={1} y={2} />);

引数をpropsで定義(省略可能)

プロパティを省略可能にするには、?をつけて定義すれば、x?: number | undefinedと定義したことになる。
プロパティを指定しなければundefinedが入るのでundefinedが入っても大丈夫なように処理をする必要がある。Object型のプロパティで、それのメソッドを呼び出す場合は、明示的にundefinedのチェックをしないとエラーになるので少し厄介。

type PropsOptional = {
  x?: number;
  y: number;
};

const ExOptional = (props: PropsOptional) => {
  return (
    <div>
      {props.x === undefined ? "なし" : props.x}:{props.y}
    </div>
  );
};

s.add("ExOptional", () => (
  <>
    <ExOptional y={2} />
    <ExOptional x={1} y={2} />
  </>
));

defaultPropsを定義すればデフォルト値の指定が出来る。このやり方だと、プロパティを省略可能ではないPropsで定義が出来るのでundefinedチェックが要らなくなる。

const ExOptional2 = (props: Props) => {
  return (
    <div>
      {props.x}:{props.y}
    </div>
  );
};
ExOptional2.defaultProps = {
  x: 1
};

s.add("ExOptional2", () => <ExOptional2 y={2} />);

型の定義だけを見れば、xが省略出来ないように見えるのが少し気持ち悪い気がする。
しかし、コンポーネントを使うときは、VSCodeの補完だとx?と表示されるし、処理の中ではundefinedにならないので、
こちらのPropsを使う方が正解な気がする?

引数ハッシュで定義

Functional Componentの定義方法として、props引数の定義ではなく、Hashで定義することも出来る。

const ExHash = ({ x, y }: Props) => {
  return (
    <div>
      {x}:{y}
    </div>
  );
};
s.add("ExHash", () => <ExHash x={1} y={2} />);

いちいちpropsを指定しなくてもプロパティにアクセス出来るのと、以下の用にデフォルト値を引数の中で指定できる所がメリットだと思う。

const ExHashOptional = ({ x = 10, y }: PropsOptional) => {
  return (
    <div>
      {x}:{y}
    </div>
  );
};
s.add("ExHashOptional", () => <ExHashOptional y={2} />);

プロパティをまとめて別のコンポーネントに渡したりする場合はprops引数を使用、簡単なコンポーネントの場合はHash指定という使い分けだろうか。

childrenプロパティを使用する場合

呼び出し元から子コンポーネントを渡す場合は、プロパティにchildrenプロパティを定義する。
childrenを省略可能にする場合は、children?と指定すればいい。

type PropsWithChild = {
  x?: number;
  y: number;
  children: React.ReactNode;
};

const ExWithChild = ({ x = 10, y, children }: PropsWithChild) => {
  return (
    <div>
      {x}:{y}
      {children}
    </div>
  );
};

s.add("ExWithChild", () => (
  <ExWithChild y={2}>
    <p></p>
  </ExWithChild>
));

FunctionComponentの型であるReact.FC(FunctionComponent)を使えば、わざわざchildrenを追加した型を定義しなくてもいい。(PropsOptionalが使える)

const ExWithChildFC: React.FC<PropsOptional> = ({ x = 10, y, children }) => {
  return (
    <div>
      {x}:{y + 1}
      {children}
    </div>
  );
};

s.add("ExWithChildFC", () => (
  <>
    <ExWithChildFC y={2} />
    <ExWithChildFC y={2}>
      <p></p>
    </ExWithChildFC>
  </>
));
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

truffleを使用した個人的boilerplate

TL;DR

truffleを使った時にフロントエンドとの連携の手順が多いので、個人的boilerplateを作成した。
ソース:ここ

動機

前までDappsの作成の時は、truffle-contractでgetInstance関数を作って、ReactのコンポーネントごとにgetInstanceを使ってコントラクトとフロントを連携していたが、truffle-contractが非推奨(?)になっているっぽいので、truffle-contractを使用しないDappを作ろうとした。
しかし、web3周りでかなり時間を食ったのでボイラープレートを作成した。

App.js

コピーしたbuildファイル群をimportした後、Appコンポーネントのstateにcontractのインスタンスを設定して、そこから.solファイルの関数を呼ぶ。今回はボタンを押して100を表示させるという非常に簡単な動作だが、もっと動作が煩雑になるとreduxやReact-Hooksを使用した方が良さそう...

// App.js
import React from "react"
import Sample from "./contracts/Sample"
import web3  from './web3/provider'

export default class App extends React.Component{

  state = {contract : null,accounts:null,storageValue:null}

  componentDidMount = async () => {
    const accounts = await web3.eth.getAccounts();
    const networkId = await web3.eth.net.getId();
    const deployedNetwork = Sample.networks[networkId];
    const instance = new web3.eth.Contract(
      Sample.abi,
      deployedNetwork.address,
      { from: web3.eth.defaultAccount }
    );
    console.log("Your account:",accounts)

    this.setState({contract:instance, accounts:accounts})
    console.log("Contract Instance :",this.state.contract)
  };

  runExample = async () => {
    const { accounts, contract } = await this.state;
    await contract.methods.setValue(100).send({from:accounts[0]})
    const response = await contract.methods.getValue().call();
    this.setState({ storageValue: response });

    console.log("Response : ",this.state.storageValue)
  };

  render() {
    return (
      <div>
        <button onClick={() => this.runExample()}>SET</button>
        {this.state.storageValue}
      </div>
    )
  }
}

動作

npm run devの後にlocalhost:3030にアクセス。
コンポーネントのstateの値をボタンを押して変化させるというだけの動作
スクリーンショット 2019-03-07 15.05.39.png
SETボタンを押すと、Metamaskが起動して,確認を押すと
スクリーンショット 2019-03-07 15.05.53.png
100がセットされた

スクリーンショット 2019-03-07 15.06.01.png

package.json

  • web3のバージョンは最新バージョンをインストールすると正体不明のエラーになることがあるので "web3": "1.0.0-beta.37"で固定している。
  • truffle migrate --resettruffle compile --allはよく使うのでscriptsに書いた。
  • npm run artifactsでjsonファイル群を/src/に持ってくる。
// package.json
{
  "name": "web3-templete",
  "version": "1.0.0",
  "license": "ISC",
  "description": "web3-templete",
  "main": "truffle-config.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "dev": "npm run artifacts && yarn start",
    "mc":"truffle compile --all && truffle migrate --reset",
    "mcr":"truffle compile --all && truffle migrate --network ropsten",
    "artifacts": "cp -r ../build/contracts/ ./src/contracts",
    "start": "react-scripts start && npm run artifacts",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "dependencies": {
    "antd": "^3.12.3",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "react-scripts": "^2.1.5",
    "react-toastify": "^4.4.3",
    "truffle-hdwallet-provider": "^1.0.2",
    "web3": "1.0.0-beta.37"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ]
}

truffle-config.js

truffle-config.jsは普通通りに書いている。
ropstenはinfura使用を想定

var HDWalletProvider = require("truffle-hdwallet-provider");
var mnemonic = process.env.MNEMONIC;
var url = process.env.INFURAURL;

module.exports = {
    networks: {

      development: {
        host: "127.0.0.1",
        port: 8545,
        network_id: "*" // Match any network id
        //gas:4712388
      },

      ropsten: {
        provider: function() {
          return new HDWalletProvider(
            mnemonic,
            url     // set your infura url
          );
        },
        network_id: 3,
        // gas: 5000000
      }
  }
}

まとめ

web3のバージョンはできれば固定しましょう。

参考
https://github.com/truffle-box
https://github.com/truffle-box/react-box

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react hooksを触って、"Hooks can only be called inside the body of a function component"が出た時の話

概要

ちゃんと<Component .../>形式で書かないとダメなようです。

経緯

先日少し作業が一段落したので、今更ながらreact-hooksを触っていました。
今まで作っていたもののうち1つを関数コンポーネント(以下FC)に置き換えようとし、さあ動確だとなったところ、

Uncaught Invariant Violation: Hooks can only be called inside the body of a function component.

というエラーが出て、上手く描画されない事態に遭遇しました。

誤解

確かに今までクラスコンポーネントで書いていた中、一部分だけを置き換えました。
また、利用側をFCに置き換えるとエラーが出ないので、「react-hooksを使うには全てFCにしないとダメなのか・・・?」と非常に焦りました。

コード例

生JSのクラスと異なりTSのクラスは色々と便利なので、FCに置き換えた際でもクラスを使うことはやめていませんでした。
(この例だと省いていますが、実際にはサポート用ロジックを色々書いていました。)

import * as React from 'react';
import * as ReactDOM from 'react-dom';

interface ChildComponentProps {
  title: string;
}

class ChildComponent { // FC形式
  public render(props: ChildComponentProps) { // このメソッドを呼び出すことで描画する
    const [title] = React.useState(props.title);
    return (
      <div>{title}</div>
    );
  }
}

class ParentComponent extends React.Component { // CC形式
  private _child = new ChildComponent();

  public render() {
    return (
      <div>
        {this._child.render({title: 'test'}}}
      </div>
    );
  }
}

ReactDOM.render(<ParentComponent />, document.getElementById('app'));

エラー原因を調査するまで、単に関数やメソッド呼び出し等で仮想DOMを返せばFCになると思っていました。

解決方法

いろいろと調べた所、下の方と同じ現象に遭遇していることに気づきました。

https://medium.com/@jonchurch/how-to-fix-react-error-rendered-fewer-hooks-than-expected-e6a378985d3c

このとき初めて、仮想DOMを関数で返すこと≠FCという根本的な誤解に気づきました。
<Component .../>等の、Reactで取り扱う形式で記述して初めてFCとなるようです。
(確かにどこが境界なのかずっと疑問だったのと、Reactの仕組みを考えると当たり前なのですが…)

幾つか解決方法があるようですが、子のクラスインスタンスを保持する形式は変えたくなかったので、下記のようにReactNodeを返すgetterを定義し、呼び出すようにしました。

class ChildComponent {
  private _node!: (props: IChildComponentProps)=>React.ReactElement; // useMemo等に影響があるので、同一の関数にする

  public get createNode() { // このメソッドを追加
    if(!this._node) {
      this._node = (props: IChildComponentProps) => this.render(props)
    }
    return this._node;
  }

  public render(props: IChildComponentProps) {
    const [title] = React.useState(props.title);
    return (
      <div>{title}</div>
    );
  }
}

class ParentComponent extends React.Component {
  private _child = new ChildComponent();

  public render() { // JSX形式にする
    return (
      <div>
        <this._child.createNode title='test' />
      </div>
    );
  }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで実装する虫眼鏡レンズ

はじめに

いいライブラリがなかったので、Reactで虫眼鏡のような拡大鏡をパワーで実装してみましたというパワー系の記事です

ポイント

ポイントはhoverした時点で、imageタグのopacityを0にし、background-imageを拡大していくことで拡大鏡を実装しています。
マウスカーソルの位置を取得して、その値に応じてtransform-originbackground-positionを変えていきます。

デモンストレーション

magnifying_glass.mov.gif

ソースコード

JS
const imageUrl = 'http://img.kb-cdn.com/imgviewer/NVpIM2ptOHhYRzVmUk5rM1NrNlFxYVV6enV4aGk2UFRJMmxPckdDUUVNYmF1RnpSNUZYSGVySnNpclp0dGpMT2xQcndrSmo1U0dxdHR4WmNjZHZoM2RKUmpwbktBZ0E5eDFOd0I0RFdsNE1XbS9NbE1QWFMxa2JCaVVDRzdNMUtNWHcrWGkxdjdUQ2Jya25ZZ2t4Z2M5MEM3MGdZUGwvTEx3RHRScVhBdzM2QjFYNVNHQ2trNDhnWUFlelBZU2Jr?square=0'

class ZoomImage extends React.Component {
  state = {
    backgroundImage: `url(${imageUrl})`,
    backgroundPosition: '0% 0%',
    transformOrigin: '50% 50%',
    transform: 'scale(1)',
  };

  handleMouseMove = event => {
    const {
      left,
      top,
      width,
      height,
    } = event.currentTarget.getBoundingClientRect();
    const x = ((event.pageX - left) / width) * 100;
    const y = ((event.pageY - top) / height) * 100;

    this.setState({
      ...this.state,
      transformOrigin: `${x}% ${y}%`,
      backgroundPosition: `${x}% ${y}%`,
    });
  };

  handleMouseOver = () => {
    this.setState({ ...this.state, transform: 'scale(2.5)' });
  };

  handleMouseLeave = () => {
    this.setState({ ...this.state, transform: 'scale(1)' });
  };

  render = () => {
    return (
      <div className="zoomImgContainer">
        <div
          onMouseMove={this.handleMouseMove}
          onMouseOver={this.handleMouseOver}
          onMouseLeave={this.handleMouseLeave}
          style={this.state}
          className="zoomImgSection"
        >
          <img
            className="zoomImg"
            src={`${imageUrl}`}
            alt="画像がありません"
          />
        </div>
      </div>
    );
  };
}

React.render(<ZoomImage />, document.getElementById('app'));
CSS(Stylus)
.zoomImgContainer
  overflow hidden
  width 416px
  height 416px
  margin 0 auto

.zoomImgSection
  background-repeat no-repeat

.zoomImgContainer:hover
  .zoomImg
    opacity 0

.zoomImg
  display block
  width 100%
  pointer-events none
HTML
<div id="app"></app>

CodePen

See the Pen React at CodePen by kazukiii (@kazukiii) on CodePen.

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React.Hooks で 大量の input 更新ハンドラーをまとめる小技

Hooks で form 周りが大分楽になりましたね。さて、useState と useCallback を使って form を作るとき、大量の input 要素向けに callbackHandler と state を用意するのって、結構大変ですよね。たった 要素が4つでも、それなりにしんどいです。

import * as React from 'react'
import { useState, useCallback } from 'react'
// ___________________________________
//
// @ Types

type Props = {
  className?: string
  firstName: string
  lastName: string
  comment: string
  address: string
  onChangeFirstName: (event: React.ChangeEvent<HTMLInputElement>) => void
  onChangeLastName: (event: React.ChangeEvent<HTMLInputElement>) => void
  onChangeComment: (event: React.ChangeEvent<HTMLInputElement>) => void
  onChangeAddress: (event: React.ChangeEvent<HTMLInputElement>) => void
}
// ___________________________________
//
// @ Component

const Component: React.FC<Props> = props => (
  <form className={props.className}>
    <input
      name="first_name"
      type="text"
      value={props.firstName}
      onChange={props.onChangeFirstName}
    />
    <input
      name="last_name"
      type="text"
      value={props.lastName}
      onChange={props.onChangeLastName}
    />
    <input
      name="comment"
      type="text"
      value={props.comment}
      onChange={props.onChangeComment}
    />
    <input
      name="address"
      type="text"
      value={props.address}
      onChange={props.onChangeAddress}
    />
  </form>
)
// ___________________________________
//
// @ Container

const Container: React.FC = () => {
  const [firstName, setFirstName] = useState('')
  const [lastName, setLastName] = useState('')
  const [comment, setComment] = useState('')
  const [address, setAddress] = useState('')
  const onChangeFirstName = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setFirstName(event.target.value)
    },
    [firstName]
  )
  const onChangeLastName = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setLastName(event.target.value)
    },
    [lastName]
  )
  const onChangeComment = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setComment(event.target.value)
    },
    [comment]
  )
  const onChangeAddress = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setAddress(event.target.value)
    },
    [address]
  )
  return (
    <Component
      firstName={firstName}
      lastName={lastName}
      comment={comment}
      address={address}
      onChangeFirstName={onChangeFirstName}
      onChangeLastName={onChangeLastName}
      onChangeComment={onChangeComment}
      onChangeAddress={onChangeAddress}
    />
  )
}

export default Container

解決策

  • state を一つの object にまとめる
  • state の key は、name属性にあわせる

こうすることで、update関数での更新処理が [event.target.name]: event.target.value で済みます。

import * as React from 'react'
import { useState, useCallback } from 'react'
// ___________________________________
//
// @ Types

type State = {
  first_name: string
  last_name: string
  comment: string
  address: string
}
type Props = {
  className?: string
  onChangeInputText: (event: React.ChangeEvent<HTMLInputElement>) => void
} & State
// ___________________________________
//
// @ Component

const Component: React.FC<Props> = props => (
  <form className={props.className}>
    {(['first_name', 'last_name', 'comment', 'address'] as const).map(name => (
      <input
        key={name}
        name={name}
        type="text"
        value={props[name]}
        onChange={props.onChangeInputText}
      />
    ))}
  </form>
)
// ___________________________________
//
// @ Container

const Container: React.FC = () => {
  const [state, update] = useState<State>({
    first_name: '',
    last_name: '',
    comment: '',
    address: ''
  })
  const onChangeInputText = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      event.persist()
      update(prev => ({
        ...prev,
        [event.target.name]: event.target.value
      }))
    },
    []
  )
  return <Component {...state} onChangeInputText={onChangeInputText} />
}

export default Container

余談

value={props[name]} が通用しているのは、このコードが TypeScript3.4 で書いているためです。
as const を使うことで String Literal Types になるので、props を参照することができています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React開発において便利なTypeScriptの型まとめ

React開発において個人的に便利だなーと思っているTypeScriptの型をだだーっとまとめてみました。私自身もまだまだTypeScript修行中の身ですので、新たに気づいたものがあったら随時追記していきます。みなさんも「こういう使い方できるぜ!」みたいなのがあったら、ぜひ教えていただければと思います。

対象とする読者

  • 最近ReactにTypeScriptを導入し始めた人
  • ReactにTypeScriptを導入してそこそこ経つけど、いまいち使いこなせてる気がしない人

TypeScriptにあまり詳しくない人でもわかるように説明しているつもりではありますが、以下の記事がTypeScriptの入門用に素晴らしいので、そちらを先に読むとスムーズに読み進められると思います。
TypeScriptの型入門

Partial

React開発においてよく定義する型としてコンポーネントのpropsの型があると思います。例えばButtonコンポーネントみたいなのがあったとして、文字や色をpropsで受け取るとします。ただし、これらは必須ではなくて、propsが渡されなかった場合は別に用意したデフォルトの値を使います。

// titleおよびcolorはstringもしくはundefinedとなる
type ButtonProps = {
  title?: string
  color?: string
}

TypeScriptでは?をつけることでそのプロパティがオプションだと示すことができます。ひとつひとつのプロパティに?を付けていくのでもいいですが、全部のプロパティをオプションにしたいときはPartialが便利です。

// 先程の例と同じ型になる
type ButtonProps = Partial<{
  title: string
  color: string
}>

これで先程の例と同じものが表現できます。今回はプロパティが2つなのでうまみを感じにくいですが、プロパティが多い場合だとひとつひとつに?をつけるのが面倒なのと、パっと見て全部のプロパティがオプションなんだなとわかるのは後者のほうだと思います。

never

TypeScriptを勉強し始めた当初、いまいち使いどころわからなかったのが、neverです。本当に使う機会がなくて、いらない子扱いしてた(ごめんよ)のですが、コンポーネントのchildren propsで使うと便利です。以下のようなコンポーネントを作ってみました。ヘッダー用のコンポーネントで、具体的な中身はchildrenで渡すようにします。

import { FC } from 'react'

type HeaderProps = { name: string }

const Header: FC<HeaderProps> = ({ name, children }) => {
  return (
    <div>
      <p>{name}さんこんにちは</p>
      <div>{children}</div>
    </div>
  )
}

const Root: FC = () => {
  return <Header name="名無し">ようこそ</Header>
}

なお、この記事ではReact.Componentをextendsしたクラス型コンポーネントではなく、React.FCを使った関数型コンポーネントを使用します。1React.FCを使うと型引数で渡した型と共にchildren?: React.ReactNodeというpropsを受け取ることができます。?がついているのでオプションになってますね。つまり、デフォルトではchildrenを渡しても渡さなくてもどっちでもいいわけですが、これはちょっと曖昧な気がします。上記のようなコンポーネントだと必ずchildrenを受け取りたいですし、逆にchildrenを受け付けたくないコンポーネントもあると思います。ちなみに、neverの代わりにundefinedを使っても同じことができますが、neverのほうが「絶対children渡すなよ!」感があって好きです。

children?: neverでchildrenを拒否する

そのようなときにpropsの型定義でchildrenを上書きすると、childrenを受け取るか受け取らないか明示することができます。HeaderPropsの型定義を以下のように変えてみます。

import { ReactNode } from 'react'

// childrenを必ず受け取る(?を取り除く)
type HeaderProps = {
  name: string
  children: ReactNode
}

// childrenを拒否する(never型にする)
type HeaderProps = {
  name: string
  children?: never
}

エディタ上で編集すればわかりますがchildren: ReactNodeにした状態でHeaderにchildrenを渡さないとコンパイルエラーになり、逆にchildren?: neverにした状態でchildrenを渡すとこれまたコンパイルエラーになります。このようにして、propsの型定義によってそのコンポーネントがchildrenを受け取るか受け取らないかを明示することができます。

Pick

続いて、コンポーネントのpropsのみならず、幅広い場所で使えるPickです。名前のイメージ通り、特定の型の中から指定したキーのプロパティのみを抽出する型です。型引数の1つ目に抽出元の型、2つめに抽出するプロパティのキー(union型(|)で複数指定可)を指定します。

type ShopItem = {
  id: string
  name: string
  shopId: string
}

// ShopItem型からidとnameのプロパティを抽出した型を生成
type Item = Pick<ShopItem, 'id' | 'name'>

// ↑と一緒
type Item = {
  id: string
  name: string
}

既存の型から新しい型を作りたい際に便利です。

Exclude

続いて、Excludeですが、こちらはPick型の逆かと思いきや少し使い方が違い、型引数の1つ目のunion型から2つ目の型(Pickと同様union型で複数指定可)を除いた型となります。ちょっと言葉ではわかりづらいので例を見てみましょう。

// 結果は'id' | 'name'
type ItemKey = Exclude<'id' | 'name' | 'shopId', 'shopId'>

id' | 'name' | 'shopId'から'shopId'を引いているので、結果は'id' | 'name'になります。これが何に使えるかと言うと、先程紹介したPickと組み合わせると、Pickの逆、つまり特定の型から指定したキーのプロパティを除いた型を作れます。これをよく、Omitと呼んだりします。残念ながらOmitはTypeScriptの公式として用意されていませんが、自分で作ることができます。

Omitを自作する

Omitの実装は以下になります。ちょっと複雑なので分解していきましょう。Tは抽出元の型、Kは除きたいプロパティのキー(union型で複数指定可)だと思ってください。

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> 

まずはkeyofというキーワードですが、keyof TでTの全てのプロパティのキーのunion型になります。例えば、以下のようになります。

type Item = {
  id: string
  name: string
}

// 結果は、'id' | 'name'
type ItemKey = keyof Item

Item型のプロパティはidとnameなので、'id' | 'name'になりますね。つまりK extends keyof Tというのは、KはTの全てのプロパティのキーのunion型の一部ということになります。TからKのプロパティを除きたいので当然(TにKのプロパティがなければKを除く意味がありません)ですね。続いて、

Exclude<keyof T, K>

この部分ですが、今までの知識を使うと「Tの全てのプロパティのキーのunion型からKを除いた型」となります。言い換えると「TからKを除いた全てのプロパティのキーのunion型」になります。これをPickの型引数の2つ目に指定すると、TからKを除いた型になるわけです。

type ShopItem = {
  id: string
  name: string
  shopId: string
}

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

// この2つは全く同じ型
type Item = Pick<ShopItem, 'id' | 'name'>
type Item = Omit<ShopItem, 'shopId'>

intersection

ここまでPick、Exclude、Omitと紹介してきましたが、実は個人的にほとんど使っていません。というのも既存の型を再利用したいときは、intersectionをよく使っています。これはA & Bとすると、AとBをマージした型を生成できるというものです。

type Item = {
  id: string
  name: string
}

type Shop = {
  shopId: string
}

// Item型とShop型をマージ
type ShopItem = Item & Shop

// ↑と一緒
type ShopItem = {
  id: string
  name: string
  shopId: string
}

PickやOmitを特定の型から指定のプロパティを除く、つまり引き算とすると、intersectionは型を組み合わせる足し算です。どちらを使うかはケースバイケースや好みにもよると思いますが、個人的には大きな型をくずして小さな型を作るよりかは、小さな型を組み合わせて大きな型を作るという後者の方がメンテナンス性が高いのではないかと思っているので、主にintersectionを使っています。

ReturnType

続いて、ReturnTypeですが、こちらは型引数に関数型を指定するとその戻り値の型を取得できるものです。例を見てみましょう。

const plus = (x: number, y: number): number => {
  return x + y
}

// number型になる
type PlusFunctionReturnType = ReturnType<typeof plus>

plusは数字を2つ引数で受け取って、両者を足したものを返すという単純な関数です。typeofでその変数の型を取得できます。つまりtypeof plusとは(x: number, y: number) => number型となります。それをReturnTypeの型引数に指定してるので、結果としてnumber型が取得できるわけです。

これをどこで使えるかと言うと、一つの例としてReduxとコンポーネントを接続する箇所です。Presentational Componentに渡すpropsとして、mapStatoToPropsとmapDispatchToPropsを定義すると思いますが、そちらで使うと便利です。

// SomeContainer.tsx
// AppStateとかDipatchとかconnectとかもろもろimportしている想定です
import SomeComponent from '../presentational/SomeComponent'

const mapStateToProps = ({ user }: AppState) => ({
  user
})

const mapDispatchToProps = (dispatch: Dispatch) => ({
  // actionをまとめたものだと思ってください
  actions: new ActionDispather(dispatch)
})

// userとactionsのプロパティを持った型になる
export type Props = ReturnType<typeof mapStateToProps> &
  ReturnType<typeof mapDispatchToProps>

export default connect(mapStateToProps, mapDispatchToProps)(SomeComponent)


// SomeComponent.tsx
import { FC } from 'react'
import { Props } from '../container/SomeContainer'

const SomeComponent: FC<Props> = ({ user, actions }) => {
  // 省略
}

注目していただきたいのは、ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>の箇所です。よくよく考えてみると、SomeComponentに渡されるPropsの型は、mapStatoToPropsの戻り値とmapDispatchToPropsの戻り値を合体させたものなわけですから、それぞれのReturnTypeのintersection型にすればいいわけです。このPropsの型を愚直に定義するとこうなります。

type Props = {
  user: User
  actions: ActionDispathcher
}

しかし、この場合だと、SomeComponentに渡すpropsを新たに増やすことを考えてみてください。mapStatoToPropsやmapDispatchToPropsに修正を加えるとともに、上記のPropsの型定義も修正しなければなりません。一方で、ReturnTypeを使う場合だと、動的に型を生成するため、mapStatoToPropsやmapDispatchToPropsに修正を加えると自動的にPropsの型定義にも反映されます!これは気持ちよくないですか?

プロパティアクセス

最後にプロパティアクセスを紹介します。プロパティアクセスとはT[K]と書くと、TのKというキーのプロパティの型を取得できるものです。例を見てみましょう。

type User = {
  id: number
  name: string
}

// number型になる
type IdType = User['id']

User型のプロパティidはnumber型なので、number型が取得できます。これをどこで使えるかと言うと、クラス型コンポーネントのpropsの型を取得することができます。

// Header.tsx
import { Component } from 'react'

type Props = { name: string }

class Header extends Component<Props> {
  // 省略
}

export default Header


// OtherComponent.tsx
import Header from './Header' 

// Header.tsxからPropsをimportしてないのに{ name: string }が取れた!
type HeaderProps = Header['props']

当たり前のことですが、クラス型コンポーネントはClassなわけですから'props'というキーにアクセスできますし、そのコンポーネントのpropsの型が取得できました。TypeScriptを使っていく中での一つの悩みとして、やたらと型をimport/exportして記述量が増えてしまうというのがあるのですが、この方法を使うとコンポーネントをexportするだけで済み、propsの型をimport/exportする必要はありません。ただ、既に述べたとおり、コンポーネントをClassで書くのはできるだけやめて、関数で書くようにしていますが、その場合だとこの方法は使えません。だって、関数ですからね。

関数型コンポーネントのpropsの型を取得する(したかった)

そのため、関数型コンポーネントでもpropsの型を取得する方法を考えたのですが、私のレベルだとこれが限界でした。

index.d.ts
type FirstArgumentType<T extends Function> = T extends (...args: infer A) => any ? A[0] : never
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> 
declare type PropsTypeFromFC<T extends Function> = Omit<FirstArgumentType<T>, 'children'>

ざっくり説明するとFirstArgumentTypeで、ある関数の最初の引数の型を取得します。T extends Functionなので、関数型コンポーネント以外の普通の関数も受け付けてしまいますが。。Omitは先程説明したものですね。FirstArgumentTypeはchildrenも含まれてしまうので、Omitでそれを除いてます。一応これで関数型コンポーネントでもpropsの型を取得できます。しかし、PropsTypeFromFC<typeof SomeComponent1> & PropsTypeFromFC<typeof SomeComponent2>のような感じで、intersectionを挟むとかなり複雑になるため、エディタでマウスオーバーしたときにうまく中身を表示してくれないというのが悩みです。これだと素直にpropsの型をimport/exportしてもいい気がしてきました。。React.FC(FunctionComponent)をimportしてごにょごにょすればうまくできそうな気がしなくもないですが、それをするとグローバルな型にできないので本末転倒ですし。

もし、これのうまいやり方がわかる偉い人がいたら教えていただけるとありがたいです。

【おまけ1】type vs interface

TypeScriptを使っていると誰しも一度は思うことだと思いますが、typeとinterfaceって結局どっちを使えばいいんだっけ?という疑問です。以下の記事が詳しいですが、実は両者はほとんど違いがなく、書き方は違えどだいだいどっちも同じことができます。

TypeScriptのInterfaceとType aliasの比較

なので、好みやプロジェクトごとのスタイルになると思うのですが、個人的にはtypeを推しています。理由としては、これまでに紹介した、型を加工するような処理を行うと、typeを必ず使わなくてはいけないシーンがあるからです。対して、interfaceを必ず使わなくてはいけないシーンには個人的にあまり出会ったことがなく、どのみちtypeを使わなければいけないのだから、最初から全部typeで統一してしまおうというモチベーションです。ただ、このへんは私もあまり詳しくないので、もし「interface使ったほうがいいよ!」という情報がありましたら教えていただけるとありがたいです。

【おまけ2】TypeScriptコードの不吉な匂い

かの有名なリファクタリングの「コードの不吉な匂い」ですが、もしTypeScriptを書いていて以下のように感じることがあれば、それは改善のサインかも知れません。

  • 型定義を変更するときにやたらと変更箇所が多い
  • やたらと型のimport/exportが多い

TypeScriptでReactを開発した感想としては、型の恩恵を受けられるのはすごく良いと思いつつも、どうしても記述量が増えてしまうというのが少し不満でした。ここまでで紹介したような動的な型を駆使すれば、ある程度そのつらみを減らすことができると思います。

この記事を読んで、みなさんが、つらくない楽しいReact + TypeScriptライフを送ってくれるようになれば幸いです。


  1. 最近導入されたReact Hooksなどを見ても、公式がステートレスで副作用のない関数型コンポーネントを推奨しているような気がします。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「StackOverflow から突然 Qiita に移動」を、App Extensions と React Native で簡単プロトタイプ

なんか実装でハマってしまい、うまくいかねーなって思いながら、StackOverflow を見ていたら、なんだか似たような質問 Qiita で見たし、やっぱり日本語で読みたいから Qiita に行こうかな、と思うことありませんか?
ありますよね!

それ、Action Extension でさくっと実装できます!
しかも、ほんの少しの Swift と Objective-C を書けば、あとは React Native で書けます。

ということでやってみました。

0. 本稿について

  • 対象者: iOS プログラミング初心者. Xcode といえば、xcode-select --installだと思っている人
  • 書いている人: iOS プログラミング初心者. Swift はprint("Hello, World")だけで卒業した
  • 必要な知識: JavaScript, React, React Native を触ったことがある
  • 動作環境: macOS X mojave. React Native 0.58.x. Xcode や Swift のバージョンはしらない
  • 内容: チュートリアル形式で Action Extension を React Native から使えるまでの実装方法を紹介する
  • 初公開日: 2019/03/06 更新日は上に書いてあるよ

1. React Native CLI のインストールとアプリのひな形の作成

では、さっそく元気に新プロジェクトつくってやっていきましょう。

$ yarn global add react-native-cli
$ cd /path/to/somewhere
$ react-native init So2Qiita

もし CamelCase なフォルダ名が気持ち悪ければ、ひな形作成後に、

$ mv SO2Qiita so2qiita

リネームしても大丈夫です。また、git にもコミットしておきます。

$ git init
$ git add -A
$ git ci -m "Create project via react native cli"

ここで一度実行してみる。
初めて起動する場合や、metro という React Native 用に Javascript の Bundle を行うプロセスが起動していない場合は cli から、

$ react-native run-ios

で起動。

ひとたび React-Native の画面のレンダリングがうまく実行できるようになれば、Objective-C や Swift を変更したとき、 Xcode からビルドすることも可能といえば可能です。
しかし、簡便のため、本記事で「実行する」と書いた場合は、CLI で、react-native run-iosしていると考えておいてください。

ひとたびビルドタスクがはじまると、マシンが熱をはらみ、CPU のほとんどを持っていかれるので、人間は大人しく wait 状態に入りましょう。

Simulator Screen Shot - iPhone 7 - 2019-03-04 at 19.47.12.png Simulator Screen Shot - iPhone X - 2019-03-04 at 19.54.00.png

さらに、App.jsxを編集して、Cmd+R すると、画面が変わることも確認できたでしょうか。

Simulator Screen Shot - iPhone X - 2019-03-04 at 19.55.09.png

2. Xcode を開いて、Action Extension を追加。

まずは言葉の定義から。

App Exntesions
  |- Share Extension
  |- Action Extension

App Extensions は総称。具体的な実装として、Share Extension と Action Extension がある。他にもあるかもしれないが知らない。

Share Extension は現在閲覧中の情報を他のアプリや SNS・サービスで共有するための拡張で、ActionExtension は現在閲覧中の情報に関連したタスクを実行するための拡張です。

このあたりの仕分けはけして自明ではなく、URL をブックマークするにしてもはてブを使うなら、Share Extension っぽいし、Safari のブックマークに残すなら、Action Extension っぽい。

実装としてはほぼ同じものになるので、Apple のガイドラインを読んだ上で、より自分の作る拡張に適した方を選んでください。

また、これらの拡張機能の設計指針についても、ガイドラインでしっかり触れられているので、軽く目を通しておいて損はない。
煩雑な UI にするなとか、単一の機能を持つように設計しろとか、まぁそういった内容。

今回は閲覧中の Stack Overflow のページからタグ情報を抜き出し、Qiita のページを開く、という動作で、Share じゃない感が強いし、Action Extension を選択します。

さて、Action Extension を作成すると決まったところで、いよいよ Xcode を開きます。
普段のお仕事がサーバーサイドだったら、Xcode を開くのは、年に数回あるかないかというところで、不慣れではありますが、怖がらないで、やっていきましょう。

open ios/So2Qiita.xcodeproj/

Xcode がおもむろにたちあがります。以下のメニューを選択して、

File > New > Target ...

Screen Shot 2019-03-04 at 19.33.06.png

ウィザードから、Action Extensionを選択。

Screen Shot 2019-03-04 at 19.33.26.png

Swift で書きたいんだけど、ここはいったん Objective-C を選んでおいてください。
名前は、So2QiitaExtとした。

Screen Shot 2019-03-04 at 19.34.06.png

Activateするか聞かれるので、当然Yes, "Activate"。

Screen Shot 2019-03-04 at 19.34.20.png

これで、空のActionExtensionができたことになる。

React Native を使えるようにするために、So2QiitaExtに依存関係として、JavaScriptCore.frameworklibRCTxxx.aが10個, それにlibReact.a というライブラリを追加します (tvOS 向けと間違いないようにしてください)。
あと、Qiita を WebView で開くので、libRNCWebView.aも追加しておいてください。

結果、こんな感じになる。

Screen Shot 2019-03-06 at 23.23.13.png

また、build phrasesの、Other Linker flags に、-ObjC -lc++を追加します。

Screen Shot 2019-03-06 at 23.24.57.png

謎めいた作業のように思われるかと思いますが、ここをきちんとしておかないと、後で詰むので、今がんばってください。

また、Deployment Info > Deployment Target が最新のものになっていると、Readt Native から run-ios したときの simulator から実行したときには deploy されないので、バージョンを合わせておきましょう。2019 年 3 月時点では、11.4 にする必要がありました。

Screen Shot 2019-03-04 at 19.59.07.png

ちなみに、asset 由来の color などが理由でビルドが失敗する場合、.xcassetsファイルは削除しても問題ありません。今回のチュートリアルでは使用しないです。

さて、ここでまた実行してみましょう。
実行後、Safari を起動して、Share ボタンをクリック。So2QiitaExt が無事に表示されていれば成功。

Simulator Screen Shot - iPhone 7 - 2019-03-04 at 20.00.48.png
Simulator Screen Shot - iPhone 7 - 2019-03-04 at 20.00.43.png

これからは毎回書かないけれど、section 毎に、git commitしておくと便利です。

3. Action Extension から React Native を呼び出す。

Xcode での作業がもう少し続きます。

いまSo2QiitaExtを開いて実行すると、画像を受け取り、そのまま表示するというデフォルトの動作になっています。
(ぜひ、Photos アプリから写真を選択して試してみてください)

Safari から実行しても画像が渡されないためなにも表示されず、空白のモーダルに、Done ボタンがあるだけです。
実はこの動作は、So2QiitaExt/ActionViewController.m内のviewDidLoadメソッドで定義されています。

このメソッドをごっそり削除して、代わりに

//  ActionViewController.m

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

をヘッダに追加してから、

//  ActionViewController.m

- (void)loadView {
  NSURL *jsCodeLocation;

  jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];

  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"So2Qiita"
                                               initialProperties:nil
                                                   launchOptions:nil];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
  self.view = rootView;
}

をというメソッドを追加してください。
viewDidLoadloadViewはなにが違うのか、気になる方は、

UIViewController のライフサイクル - Qiita

などを参照のこと。

さて、これで実行し、Safari から Share ボタン経由で、So2QiitaExtを立ち上げると空のモーダルが出現します。
ふむ、これはなにかおかしいですね。
本来なら、コンテナアプリの起動画面と同様に"Welcome to React Native"が出て欲しいところです。

実はセキュリティ保護の観点からデフォルトでは localhost も含めて http 通信はブロックされており、かつ React Native は開発環境下では Hot Reload やデバッグを有効にするために、http で js.bundle をダウンロードして実行しているのです。

ということで、So2QiitaExtInfo.plist に以下の項目を追加します。

Screen Shot 2019-03-06 at 23.30.13.png

このXMLをコピペしてもいいよ。

<key>NSExceptionDomains</key>
<dict>
  <key>localhost</key>
  <dict>
    <key>NSExceptionAllowsInsecureHTTPLoads</key>
    <true/>
  </dict>
</dict>

そして、再実行。
今回はウェルカムメッセージがモーダル内に表示されたのではないでしょうか。

Simulator Screen Shot - iPhone X - 2019-03-04 at 21.53.40.png

しかし、なんと、モーダルを消すすべがありません。
いまのところ、「上へスワイプ」から Safari を kill することでなんとか終了してください。

4. Action Extension から呼び出されたとき用の画面を作る

モーダルを消す方法は少し先延ばしにして、ここからは Action Extension 用の画面を作っていきましょう。
方策としては、

  1. initialPropertiesに Action Extension からのリクエストの場合のみ True になるフラグを設定して分岐する
  2. エントリーポイントとなるファイルをindex.jsではないなにかに変える

が考えられます。正直、どちらでもかまわないのですが、今回は特にファイルを分けるほどの理由もないので、方策 1 を採用しましょう。

loadView メソッドに

//  ActionViewController.m

NSDictionary *initialProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool: TRUE] forKey:@"isExtension"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                    moduleName:@"So2Qiita"
                                             initialProperties:initialProps
                                                 launchOptions:nil];

を追加。
RCTRootView のイニシャライザに、 initialProperties として、{isExtension: true}を渡すようにします。

そして、React Native 側では、

// App.js

export default class App extends Component<Props> {
  render() {
    const { isExtension } = this.props;
    let message;
    if (isExtension) {
      message = "Welcome to So2QiitaExt on React Native!";
    } else {
      message = "Welcome to So2Qiita on React Native!";
    }

    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>{message}</Text>
      </View>
    );
  }
}

isExtensionによって、表示するメッセージを変えてみます。実行。

Simulator Screen Shot - iPhone X - 2019-03-04 at 22.07.25.png

やりましたね。

5. モーダルを終了するためのボタンを作る

では、次に、モーダルを閉じるためのボタンを作りましょう。
今までは iOS からのアクションやメッセージを受け取るだけでしたが、今回はじめて React Native 側から、iOS へメッセージを送ることになります。
これを実現するためには、iOS 側でメッセージを受け取るためのインタフェースを用意する必要があります。

せっかくですので、Objective-C ではなくて、Swift でそのブリッジ部分を作ってみましょう。
ここがたぶん、このチュートリアルの一番難しいところ。
みなさん、無事に乗り切ってください。

まずは、Swift ファイルをSo2QiitaExtに追加します。New Fileから、Swift を選び、ActionExtensionという名前でファイルを作ります。

Screen Shot 2019-03-04 at 22.45.21.png

ブリッジファイルを作るかと聞かれるので、Create Bridging Header を選んで、作成してください。

Screen Shot 2019-03-04 at 22.45.32.png

まだファイルの中は変えなくていいです。
同名の Objective-C ファイル、ActionExtensionも作りましょう。

こういったファイルがSo2QiitaExt以下に追加されているはずです。

Screen Shot 2019-03-04 at 22.57.56.png

ここでブリッジ部分の実装を行いますが、Swift だけでは完結せず、Objective-C から、モジュールやメソッドを Extern 宣言、すなわち外部公開する必要があります。

ActionExtensionというクラスと、doneというメソッドを定義して、Javascript からアクセスできるようにしてみましょう。

まずはブリッジヘッダに必要なヘッダを追加

// So2QiitaExt-Bridging-Header.h
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//
#import "React/RCTBridgeModule.h"
#import "ActionViewController.h"

外部公開用の宣言を追加

//  ActionExtension.m
#import "React/RCTBridgeModule.h"

@interface RCT_EXTERN_MODULE(ActionExtension, NSObject)

RCT_EXTERN_METHOD(done)

@end

そして、Swiftで実装

// ActionExtension.swift

import Foundation
import os.log

let log = OSLog(subsystem: "com.o3c9.so2qiita", category: "ActionExtension")

@objc(ActionExtension)
class ActionExtension: NSObject {
  @objc
  func done() {
    os_log("done", log: log, type: .default)
  }
}

os_logを仕込むと、Console.appにログを吐き出すことができるようになります。
これが今後君の命綱となる。

そして、App.jsに、doneメソッドを呼び出すコードを追加しましょう。

// App.js

export default class App extends Component {
  _onPress() {
    NativeModules.ActionExtension.done();
  }

  render() {
    ...

    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>{message}</Text>
        {isExtension && <Button onPress={this._onPress} title="Done" />}
      </View>
    );
  }
}

そんでもって実行〜。

Donwをタップすると、無事にSwiftのdoneメソッドが呼ばれ、ログに表示されていますね。地味ですが大きな意味を持つ一歩です。

Screen Shot 2019-03-05 at 21.35.12.png

では、doneの実装を行い、本当にモーダルを閉じることができるようにします。

まずは、ActionViewController.hactionViewControllerインスタンスの外部公開と、doneメソッドの宣言を追加します。

// ActionViewController.h

#import <UIKit/UIKit.h>

@interface ActionViewController : UIViewController

extern ActionViewController * actionViewController;

- (void) done;

@end

実装ファイルでは、actionViewControllerに値をセット。done メソッドは初めに作成したひな形にすでに実装されてあるので、このままこれを流用して Swift から呼ぶという算段です。

//  ActionViewController.m

ActionViewController * actionViewController = nil;

@implementation ActionViewController

- (void)loadView {
  ...

  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:0.0f blue:0.0f alpha:0.3];
  self.view = rootView;
  actionViewController = self;
}

で、ActionExtension.swift。

// ActionExtension.swift

class ActionExtension: NSObject {
  @objc
  func done() {
    os_log("done", log: log, type: .default)
    actionViewController.done()
  }
}

はい、また、実行。

qiita_action.gif

よかったね、無事モーダルが隠れました。

6. App Extensions から現在の URL を受け取る

ずいぶん長くなりましたが、このセクションが本機能の肝心な部分。Action Extension が起動された元のアプリから URL を JavaScript で受け取ります。
今回の場合、Safari から StackOverflow の Question の URL が受け取りたい情報ですね。

まずは、JavaScript 側の実装のイメージ。関数のcallbackを使う場合。

NativeModules.ActionExteion.url( (error, url) => if(!error) this.setState({ url }));

Promiseとして受け取る場合.

NativeModules.ActionExteion.url()
  .then( (url) -> this.setState({ url }))
  .catch(e => console.log(e));

どっちでもいいんだけど、好みで、Promise でやってみましょう。
async awaitを使ってモダンにやるぞ。

まずはurlというメソッドを定義し、Promise を返すメソッドであるという宣言をブリッジ部分に書く。おまじないです。

// ActionExtension.m

RCT_EXTERN_METHOD(url: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject)

そして、Swift ファイルでの実装。

// ActionExtension.swift

@objc(ActionExtension)
class ActionExtension: NSObject {
  ...

  @objc
  func url(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void {
    guard
      let inputItem = actionViewController.extensionContext?.inputItems.first as? NSExtensionItem,
      let attachments = inputItem.attachments
      else {
        let error = NSError(domain: "", code: 400, userInfo: nil)
        reject("E_URL", "cannot obtain url", error)
        return
    }

    for provider in attachments {
      if provider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
        provider.loadItem(forTypeIdentifier: kUTTypeURL as String, options: nil, completionHandler: { (target, error) in
          let url = target as! URL
          resolve(url.absoluteString)
        })
        break
      }
    }
  }
}

まぁ、こういうの、正直すべてを正しく理解するのにはたくさん寄り道をせなあかんので、全部は説明しきれないです。
ただ Swift や iOS プログラミングに関心があれば、ぜひ Apple の公式ドキュメントや有志の解説記事を探して読んでみてください。

React Native とのつなぎこみに関しては、React Native の Official のドキュメントか、Medium で人気 TeaBreak の記事、Swift in React Native - The Ultimate Guide Part 1: Modulesが詳しいです。

// App.js

async componentDidMount() {
  try {
    const url = await NativeModules.ActionExtension.url();
    this.setState({ url });
  catch(error) {
    console.log(error);
  }
}

また、実行。

Simulator Screen Shot - iPhone X - 2019-03-05 at 22.39.11.png

やった、URLが表示されてますね。これで、Action Extension から URL を React Native の JavaScript で受け取ることができるようになりました。
めでたし、めでたし。

Action Extension を React Native と組み合わせて使う、という書きたかった内容はここまででほとんどすべて。

  • doneメソッドで、JS から Swift 側へメッセージを送る。
  • urlメソッドで、JS から Swift 側へデータを要求し受け取る。

この 2 つができるようになったわけです。

あとは、機能を完成させるために React Native 側での実装をさくっと紹介して終わることにします。

7. StackOverflow のタグを使って Qiita で検索する JavaScript

Safari から送られてきた URL を使って、StackOverflow のタグを Qiita 検索に使うというロジックの実装に入ります。

まずは、Component での実装のイメージ。
(補足すると、今の時点ではあくまでイメージだったはずが、この後、このイメージに沿って実装が進むため、結局これがそのまま Component の実装となっていく)

async componentDidMount() {
  try {
    // URLをNativeModules経由で、上記のActionExtensionクラスから受け取る & Wait
    const url = await NativeModules.ActionExtension.url();
    // そのURLをStackOverflow APIを実装したクラスに渡して、タグを受け取る & Wait
    const tags = await new StackOverflow(url).getTags();
    // タグからQiitaの検索クエリを構築
    const query = encodeURIComponent(tags.map(t => `tag:${t}`).join(" "));
    const uri = `https://qiita.com/search?utf8=%E2%9C%93&q=${query}`;
    // stateにつっこむ
    this.setState({ isLoading: false, uri });
  } catch (error) {
    this.setState({ isLoading: false, error });
  }
}

StackOverflow.jsというクラスを作って、ダミーの実装をする.

export default class StackOverflow {
  constructor(url) {
    this.url = url;
  }

  getTags() {
    return Promise.resolve(["javascript", "reactjs"]);
  }
}
class Extension extends Component {
  render() {
    if (isLoading) {
      return (
        <SafeAreaView style={styles.container}>
          <ActivityIndicator size="large" color="#0000ff" />
        </SafeAreaView>
      );
    } else if (uri) {
      return (
        <SafeAreaView style={styles.extension}>
          <Text>{uri}</Text>
        </SafeAreaView>
      );
    } else {
      return (
        <SafeAreaView style={styles.container}>
          <Text style={styles.error}>{error}</Text>
        </SafeAreaView>
      );
    }
  }
}

次に、Qiitaの検索結果ページを、WebView で表示させてみる。
WebViewはReact Native本体から分離されて別パッケージになったようなので、yarnで追加。

yarn add react-native-webview
react-native link react-native-webview

Xcode のビルド設定を見て、So2QiitaExtlibRNCWebView.aが含まれているか確認しよう。
入ってないと、"Invariant Violation: requireNativeComponent: "RNCWebView" was not found" が襲いかかってくる。

あと、全面 WebView だとせっかくつくったdoneメソッドを呼べなくなるので、NavigationBarも追加しておこう。

yarn add react-native-navbar

stateuriがあるときの View はこんな感じになる。

<SafeAreaView style={styles.extension}>
  <NavigationBar
    title={{ title: "So2QiitaExt" }}
    leftButton={{ title: "Done", handler: this._onPress }}
  />
  <WebView style={styles.webview} source={{ uri }} />
</SafeAreaView>

うまくいけば、たぶん見慣れたサイトがモーダル上に表示される。

Simulator Screen Shot - iPhone X - 2019-03-06 at 23.44.34.png

最後に、StackOverflow.getTags()をちゃんとした実装にする。
StackAppsというところから、API Keyの登録をしなくちゃいけないと思っていたが、実はこのエンドポイントはpublicなようで、認証なしで呼べる。

APIの詳細は、この辺に転がってます。

constructor(url) {
  this.questionId = this._parseUrl(url);
}

getTags() {
  return new Promise(async (resolve, reject) => {
    if (this.questionId) {
      try {
        const response = await fetch(
          `https://api.stackexchange.com/2.2/questions/${this.questionId}?site=stackoverflow`
        );
        if (response.ok) {
          const json = await response.json();
          const tags = json.items[0].tags;
          return resolve(tags);
        } else {
          return reject(`http error: ${response.status}`);
        }
      } catch (error) {
        return reject(error.message);
      }
    } else {
      return reject("not a valid SO url");
    }
  });
}

URL_REGEX = /^https\:\/\/stackoverflow\.com\/questions\/(\d+)/;

_parseUrl(url) {
  const result = url.match(this.URL_REGEX);
  return result && result[1];
}

はい、これで完成 :sparkles:

so2qiita.gif

どうだ、このセクションでは、Js しか書いてないぞ!
Test だって、Jest で書けるぞ!

ということで、React Native から、Action Extension を使う方法を紹介しました。
ごくろうさまでした。

最終的な成果物は、

https://github.com/o3c9/so2qiita

ここで公開しています。
参考になったと思えば、ぜひStar :star: をよろしくです!

8. FAQ -結びにかえて

それ、全部 Swift で書いた方が早くない?

ほとんどの場合そうかもしれないが、コンテナアプリが React Native で書かれている場合、App Extensions でもその資産を使いたいことはあるはず。そういった場合には、なるべくすべて React Native にしておくほうがいいはずなので、ここで紹介したテクニックは使えると思う。

この機能、微妙じゃね?

タグ検索だけだと微妙だけど、タイトルから重要なキーワードを推測して適切な検索クエリを構築できるようになると、けっこう実用的だと思っている。それなりに自然言語処理がんばってやらないと使い物にならないだろうけどね。

エンジニアは全員英語で読み書きできるべきだし、日本語のQiita見るより英語のStack Overflowを参照するべきでは?

You Qiita2So 作るべき

Share Extension の例もほしい

表示されるカラムなどが違い、UI としては別物に見えるけど、実質ほぼ同じものなので、Action Extension が作れたら、Share Extension も作ることができる #はず #未確認 #誰かやってみて #コメント欲しい

チュートリアル通りやっても動かない

うん、それが現実。

https://github.com/o3c9/so2qiita に完成したコードあるから、これcloneして動かしてみてください。

途中経過の再現については Xcode の設定や、コードをにらめっこしながら、差分を見つけてみましょう。
簡単じゃないけど、難しくもないはず。時間はかかるけど、こういうのがいい勉強になるよ。

Xcode 上での作業が多い、もっとラクにできないの?

そう思ってた頃もあった。

React Native Share Extensionというものがあって、わざわざライブラリになっているにもかかわらず、README に書いてある Setup のプロセスがめっちゃ長くて大変そう。
つまり、ここに書いたくらいのステップがほぼ現在のところ最短だと思う。
一回やってみて要領つかめたら、次からは怖くない

Extension のデバッグつらくない?

うん、つらい。

NativeModules.ActionExtension.xxxが不要な場合には、Extension の画面をコンテナアプリのトップ画面にして開発すると、JS 由来の動作確認がしやすくなる。

Extension として実行しているときは、console.log の代わりに、自前の logger を作って、React Native の画面に出してしまうというのをやっていた。こんな感じ。

debug(message) {
  this.setState( { console: [...this.state.console, message] })
}

render() {
  return (
    <View>
      ...
      {this.state.console.map(m => <Text>{m}</Text>)}
    </View>
  )
}

iOS 側は、os_logを使えば、Console.app から見えるようになる。
他の大量のメッセージにかき消されるという苦難はあるけれど。
Process を限定してみれば、いちおう追えないことはない。

Android は?

また今度。
https://github.com/o3c9/so2qiita のStarの進捗次第かな

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む