20191130のReactに関する記事は6件です。

【 初心者向け】ReactNativeで電卓アプリを作ろう【 チュートリアル】

こんにちは!DMM WEBCAMPでメンターをしている人です!
アドベントカレンダー16日目は、(僕の中で)最近話題のReact Nativeを使って電卓を作ってみます。

チュートリアル形式でReact未経験の方でも分かるように専門用語を出来るだけ削って書いていきます。

是非トライしてみてください!

1.React Nativeとは??

Facebookが開発した「React」というJavaScriptのフレームワークの亜種で、iOSやAndroidのアプリを作ることが出来る代物です。Reactは仮装DOMが実装されているので表示が早いというメリットがあります。

・jsなら任せて!
・Reactならわかるぞ!
・さくっとアプリ作りたい!
・スマホのアプリが作りたいけどSwift・Kotlinやる時間がない・・・
なんて方にオススメのアプリケーションフレームワークです。

さらに踏み込んだ解説が知りたい方はこちらの記事がオススメです。
「React Nativeとは何なのか」

2.今回やること

この記事では、四則演算が出来る電卓アプリを作っていきます。

ezgif.com-video-to-gif (2).gif

3.開発環境&構築

・Androidの開発を行う場合は、「Android studio」
・iOSの開発を行う場合は、「Xcode」が必要になります。

今回はiOSのアプリを作るのでAndroidの環境構築は省かせていただきます。ごめんなさい:rolling_eyes:
(Androidの開発が行いたい方はこちらの記事が分かりやすかったです!)

まずはnodeとwatchmanというものをインストールします。

$ brew install node
$ brew install watchman

次にexpoのダウンロードです。
nodeをインストールしたときにnpmが入っているのでnpmコマンドが使えます。

$ npm install expo-cli --g

Expoを深く知りたい方はこちらの記事がGoodです。

これで完了!といいたいところですがExpoはXcodeがないと開けないため、Xcodeのダウンロードをしましょう。
Xcodeで開いたiOSシュミレーター(PC上で開かれるiPhone)の中にExpoアプリがインストールされるのでXcode必須です。

Xcodeとは

XcodeはAppleが開発したソフトウェア開発者向けの主にMacやiPhone、iPadのアプリケーションを作るのに向いています。XcodeのおかげでMac上でiOSのシュミレーターが起動できます。

ダウンロードしたことがない方はApple Storeからダウンロードしてください!

ただ、めちゃくちゃサイズがデカいのでインストールに時間がかかります。

待ち時間にReactNativeの公式ドキュメント でも読んで時間を潰しちゃいましょう。

ここまできたら準備完了です!

プロジェクトの新規作成

以下のコマンドを実行してReactNativeアプリを新規作成しましょう!

$ expo init お好きな名前

プロジェクトのテンプレート選択

expo init ○○○後に、画像のようにテンプレートをどうするか聞かれますがここは「blank」を指定しましょう。
blankのスクリーンショット

名前の設定をしよう

スクリーンショット 2019-12-11 12.09.42.png
テンプレート選択した後はこのような画面になると思います。

ここでは実際のアプリ名を設定します。(アプリアイコンの下に表示される名前)

リリースを考えているアプリの場合変な名前にしてはいけません!!!

Yarnは使わなくてOK!

Yarn v1.19.1 found. Use Yarn to install dependencies? (Y/n) 

このような質問がされる場合があります。
これは「Yarnが見つかったけどYarnは使う?どうする?」と聞かれています。
今回はnpmを使用しているので「n」を押してEnterです。

最終行にこのような記述があればアプリの作成完了です!

:(省略)
:
Your project is ready at 〜〜略〜〜〜
To get started, you can type:

  cd アプリ名
  npm start

npm start を実行してこの画面が表示されれば下ごしらえも完了です!
start.jpg

Railsに比べてシックで落ち着いたスタート画面ですね、おしゃれ。

補足:自分のスマホで開いてみよう

なんと自分の携帯にも開発しているアプリを開くこともできます。

まず。ご自身のスマホにExpoのアプリ(AppleStoreもしくはGooglePlayにて「Expo」で検索です!)をインストールします。

次に、アプリを読み取るQRコードをための作業をPCでします。

アプリが立ち上がった後のターミナルで

Press ? to show a list of all available commands

と出たら、「?」を入力します。次に「d」を押すとブラウザでデベロッパツールが開かれます。

デベロッパーツールにあるQRコードを読み取ると作っているアプリのURLが出るのでタッチしましょう!これでExpoアプリが開かれます!!

4.実装!

まず初めに、開発するアプリケーションの階層から見ていきましょう。

.expo
 ├ packager-info.json
 ├ setting.json
.expo-shared
 ├ assets.json
.assets
 ├ icon.png
 ├ splash.png
.node_module
 ├ 色々入ってます
.gitignore
App.js ← 主にこれを使います
babel.config.js
package-lock.json
package.json
style.js ← こちらを新規作成します

5.アプリ作成後に書かれている記述の意味は?

アプリの新規作成後にデフォルトで開かれるのは「App.js」というファイルです。そこにいろいろな記述を書いていき、最終的にはかっこいい電卓を作ります。

では、App.jsに最初から書かれている記述を見ていきましょう。

App.js
import React from 'react'; // ①
import { StyleSheet, Text, View } from 'react-native';  // ②

export default function App() {  // ⑤
  return (
    <View style={styles.container}> // ③
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  );
};

const styles = StyleSheet.create({ // ④
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

importってなに?

①と②はReact Nativeを使って開発するときのおまじないのようなものです。

①は、"react"モジュールからimport(=輸入する)して、Reactの記述を使用できるようにしています。

アプリケーションの中にnode-moduleというフォルダがあり、更にその中のreactというフォルダからimportしてきて色々読み込んでいます。

②は、"react-native"モジュールからimportして、StyleSheetやTextクラスを使えるようにしています。

こちらもnode-moduleの中のreact-nativeフォルダからimportしてきています。

「なんで全部importしておかないの?」と思った方いませんか?僕は思いました。

実は、全ファイルやコンポーネントをimportするより、各々が使いたいファイルやコンポーネントを選んでimportする方が読み込み速度が上がるらしいです。言われてみれば納得です:writing_hand_tone1:

Reactの描画速度が早いと言われる理由はこんなところにもあるんです!

コンポーネントについて

React Nativeでは描画するために独自のコンポーネントを使用する必要があります。
理由としては、divやspanタグが使えないので、Viewなどのコンポーネントを使う必要があるというわけです。
(この概念が難しいと言うか、コードを理解不能にしている要因の1つですが、慣れると余裕です:beer:

現状のコードの中だと、VIewTextStyleSheetがReact Nativeのコンポーネントです。

importせずにこれらのコンポーネントを使用するとエラーを吐いてしまいます。

だから自分が使用したいコンポーネントに合わせて②に追記していく感じです。

例えば入力フォームを作成したい場合は、以下のように追記します。

App.js
import React from 'react'; 
import { StyleSheet, Text, View, TextInput } from 'react-native';  //←追加!!!

export default function App() {  
  return (
    <View style={styles.container}>
      <Text>Open up App.js to start working on your app!</Text>
        <TextInput style={styles.input} />  //←追加!!! 
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {     //←追加!!!
    width: 100,
    borderColor: "gray",
    borderWidth: 1,
  },
});

 <TextInput />  <TextInput> ~~ </TextInput>の省略系です、便利!

コンポーネントはたくさん種類があり公式ドキュメントにたくさん載っています。

StyleSheet.createってなに?

続いて、③と④についてです。この2つはスタイル(見た目)を担っている部分です

何をしているかというとStyleSheetというコンポーネントを作成し、その中にスタイルの記述をたくさん書いています。

では、文字を赤くさせたい!なんて時はどうすればよいでしょうか。

3ステップです。
1. stylesの中に、新しく「redColor」を定義します。(ここの命名は自由です)
2.「redColor」を反映させたいコンポーネントにstyle={styles.redColor}を足します
3. 完成

App.js
import React from 'react'; 
import { StyleSheet, Text, View, TextInput } from 'react-native'; 

export default function App() {  
  return (
    <View style={styles.container}>
      <Text style={styles.redColor}>Open up App.js to start working on your app!</Text>   //← 追記!!
        <TextInput style={styles.input} /> 
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  input: {     
    width: 100,
    borderColor: "gray",
    borderWidth: 1,
  },
   redColor: {        //← 追記!!!
    color: "red" 
  },
});

いちいちこんなことするの面倒臭い!って方には、コンポーネントに直接スタイルを書くのがオススメです。こんな風に直書きができます。

   <TextInput
     style={{width:100, borderColor: 'gray', borderWidth: 1 }} 
   />

ハマりポイント スタイルはキャメルケースで!

注意点としてはReact Nativeではプロパティーの部分はキャメルケースのみ使えます。

キャメルケースとは、「backgroundColor」のような、単語と単語の間にスペースや"-"を置かずに、文字の始まりを大文字にする形式です。
(ラクダのこぶに似てることからキャメルケースと呼ばれます。他にもスネークケース、ケバブケースがあります)

普段「background-color」としていたところを「backgroungColor」としなければなりません。

ようやくコンポーネントが描画される!

export default function App() {  // ⑤
  return (
    <View style={styles.container}> 
      <Text>Open up App.js to start working on your app!</Text>
    </View>
  );
};

ラストです。
ここではimportしたViewとTextコンポーネントを使用しています。

HTML/CSS経験者であればこの書き方に違和感を感じますよね。

意訳ですが、HTML/CSSに変換して記述してみるとこんな感じです。
コンポーネントの使い方とイメージがつかめば全然怖くありません。

意訳.html
<div style="省略">
  <p>Open up App.js to start working on your app!</p>
</div>

続いて、以下の部分です。

export default function App() {  // ⑤

};

exportとは先ほど出てきたimportの反対の意味です。

export内で定義した記述を他の場所で使えるようにするものです。

例えばsample.jsを新規作成し、そこにexport~~の記述をしたコンポーネントを2つ書きます。

これをApp.jsで受け取りたいとしましょう。

sample.js
import React, {Component} from 'react';
import {Text, View} from 'react-native';

export class Sample1 extends Component {
    render(){
        return(
            <View>
              <Text>サンプルの1です</Text>
            </View>
        );
    };
};

export class Sample2 extends Component {
    render(){
        return(
            <View>
              <Text>サンプルの2です</Text>
            </View>
        );
    };
};
App.js
import React from 'react'; 
import { StyleSheet,  View,  } from 'react-native';
import {Sample1 , Sample2} from "./sample"; //←ここでimportしています


export default function App() {  
  return (
    <View style={styles.container}>
      <Sample1 />  //←ここでimportしたSample1を
      <Sample2 />  //←ここでimportしたSample2を使っています
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

defaultオプション付きのexportは、そのファイルの中においてデフォルトでexportされるコンポーネントを決めることができます。default付きexportはimportする時の名前はなんでもよく、ついていないものはclass名をimportする側で同じにしなければなりません。(面倒くさい)

例えば先ほどのSample1クラスにdefaultを設定します

sample.js
export default class Sample1 extends Component {
//略

次にApp.jsでimportします。

App.js
import hogehoge ,{Sample2} from "./sample"; //←import名を適当にしてます

//略

    <View style={styles.container}>
      <hogehoge />  //←ここでimportしたhogehogeを使える!
      <Sample2 />  
    </View>

defaultが付いていないものは{ }で囲う必要があるので要注意です!

export defaultについて詳しくはこちらです。

6.スタイルを適応させよう

次にレイアウトを整えるためにスタイル(≒CSS)を作っています。

ここはそれほど重要ではないので、何をしているか理解したらコピペしちゃって大丈夫です。

また、App.jsにスタイルの記述をしてもいいのですが個人的に見辛いのでファイルを分けました。

2ステップです。

①App.jsと同じ階層にstyle.jsを新規作成する

②以下のコードを貼り付ける

style.js
import { StyleSheet } from "react-native";

const styles = StyleSheet.create({
    container: {
      backgroundColor: '#FFF',
    },
    countArea: {
      marginTop: 140,
      justifyContent: "center",
      alignItems: "center", 
      marginBottom:10
    },
    buttonArea: {
      marginTop: 5,
      justifyContent: 'center',
      alignItems: 'center'  
    },
    value: {
      fontSize: 30
    },
    topbutton: {
      marginTop: 50
    },
    button: {
      marginTop: 5,
      justifyContent: 'center',
      alignItems: 'center',
      width: 50,
      height: 40,
      borderRadius: 5,
      borderWidth: 1,
      borderColor: '#fff'
    },
    buttonText: {
      fontSize: 25,
      color: '#fff'
    },
    buttonUp: {
      backgroundColor: '#A53434',
      fontWeight: "bold"
    },
    buttonUpText: {
      backgroundColor: 'red'
    },
    buttonMinus: {
      backgroundColor: '#343EA5'
    },
    buttonTimes: {
      backgroundColor: 'purple'
    },
    buttonDivide: {
      backgroundColor: "green"
    },
    buttonReset: {
      borderColor: '#AFADAD',
      marginBottom: 200
    },
    buttonResetText: {
      color: '#000',
      fontSize:11
    },
    buttonNum: {
      backgroundColor: 'grey',
      fontWeight: "bold",
      justifyContent: 'center',
      alignItems: 'center',
      flexDirection: 'row',
      width: 60,
      height: 40,
      borderRadius: 5,
      borderWidth: 1,
      borderColor: '#fff'
    },
    buttonEqual: {
      backgroundColor: "black"
    }
  });

  export default (styles);

"react-native”から”StyleSheet”コンポーネントをimportしています。

こうすることによって

3行目でStyleSheet.createが使えるようになります。

この記述をすることによって、このコンポーネントはスタイルの記述をしますよ〜!と明示することが出来ます。
(StyleSheet.createしなくても動作はしますが、一応書いたほうがいいです。詳しくはこちら の記事です。)

そして、最終行では、このstyle.jsがどこでも使えるようにexportしています。

これをApp.jsでimportすればスタイルの土台は完成ですね。

7.電卓のレイアウトを作ろう

スタイルを作成したので、次はそれを適応させるためのコンポーネントを作っていきます。
App.jsに書いている記述は1度全て消してから以下のコードを書いていきましょう!ここからスタートです!!

まずは先ほどのstyle.jsをApp.jsでimportします。

ついでに、この後使う「TouchableOpactity」というコンポーネントもimportしておきます。

App.js
import React, {Component} from 'react';
import { Text, View, TouchableOpacity } from 'react-native'; //←StyleSheetとInputTextがある方は削除でOK!
import styles from "./style"  //←追記!

次にコンポーネントの作成を行っていきます。

App.js
//↓import ~~~の下に↓

export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>
        </View>
      );
    };
};

 画像のようなものが画面に写ればOKです!
スクリーンショット 2019-11-29 15.38.39.png

計算用ボタンの設置

次に電卓のボタンを作っていきましょう。

先ほどimportした「TouchableOpacity」がここで出番です。

TouchableOpacityとは、簡単に言うとクリックできる要素のことです。
これを用いることでボタンが簡単に作れます。

App.js
//↓import ~~~の下に↓

export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>

          <View style={styles.buttonArea}>
            <View style={{ flexDirection: 'row'}}>
               <TouchableOpacity style={[styles.button, styles.buttonUpText]}>
                 <Text style={styles.buttonText}>+</Text>
               </TouchableOpacity>
            </View>
           </View>
        </View>
      );
    };
};

画像のようなボタンが表示されたでしょうか?

スクリーンショット 2019-11-29 16.26.34.png

あとは、数字のボタンや四則演算のボタンを設置していきます!

一気に書いていきましょ〜!

App.js
export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>

          <View style={styles.buttonArea}>
//足し算ボタン
            <View style={{ flexDirection: 'row'}}>
               <TouchableOpacity style={[styles.button, styles.buttonUpText]}>
                 <Text style={styles.buttonText}>+</Text>
               </TouchableOpacity>
//引き算ボタン
               <TouchableOpacity style={[styles.button, styles.buttonMinus]} >
                 <Text style={styles.buttonText}>-</Text>
               </TouchableOpacity>
//掛け算ボタン
               <TouchableOpacity style={[styles.button, styles.buttonTimes]} >
                 <Text style={styles.buttonText}>x</Text>
               </TouchableOpacity>
//割り算ボタン
               <TouchableOpacity style={[styles.button, styles.buttonDivide]} >
                 <Text style={styles.buttonText}>÷</Text>
               </TouchableOpacity>
//=ボタン
               <TouchableOpacity style={[styles.button, styles.buttonEqual]} >
                 <Text style={styles.buttonText}>=</Text>
               </TouchableOpacity>
//リセットボタン
               <TouchableOpacity style={[styles.button, styles.buttonReset]} >
                 <Text style={[styles.buttonResetText]}>リセット</Text>
               </TouchableOpacity>
             </View>
           </View>
        </View>
      );
    };
};

スクリーンショット 2019-11-30 16.40.46.png
画像のようにボタンが6つ表示さえれば計算ボタンは完成です。

数字ボタンの設置

次は0〜9の数字ボタンを作っていきます。

先ほどの計算ボタンはボタンごとに色や文言が変わるので個別に作成しました。

しかし、0〜9の数字ボタンは値やボタンの文字が変わるだけです。なので繰り返し処理を使って簡潔に記述しましょう!

まずは、以下のように配列を定義してあげます。

App.js
//〜〜〜略
exporet ~~
  render() {
    return (
      <View>
      //略
      </View>
    );
  };
};
//↓ここの下に記述

const ZeroToFour = [];
for (let i = 0; i< 5; i++) {
  ZeroToFour[i] = i
};

const FiveToNine = [];
for (let i = 5; i< 10; i++) {
  FiveToNine[i] = i
};

まずは繰り返し処理に使う配列を2つ用意します。ZeroToFourは0〜4、FiveToNineは5〜9のボタンを作ります。
(レイアウト調整のためここは敢えて配列2つ使っています、10個まとめて作るとレイアウトが綺麗に整えられませんでした。甘えです、ごめんなさい)

App.js
//略
export default class App extends Component {
    render() {
      return (
        <View>
          <View style={styles.countArea}>
            <Text style={styles.value}>0  ?  0  =  0</Text>
          </View>

          <View style={styles.buttonArea}>

//↓追加↓
            <View style={{flexDirection: 'row'}}>
              {ZeroToFour.map(i => (
                  <TouchableOpacity style={styles.topbutton,styles.buttonNum} key={i} >
                    <Text style={styles.buttonText}> {i} </Text>
                  </TouchableOpacity>
              ))};
            </View> 

            <View style={{ flexDirection: 'row'}}>
              {FiveToNine.map(i => (
                  <TouchableOpacity style={styles.topbutton, styles.buttonNum} key={i}>
                    <Text style={styles.buttonText}> {i} </Text>
                  </TouchableOpacity>
              ))};
            </View>

//ここより上を追加
//足し算ボタン
            //<View style={{ flexDirection: 'row'}}>
              //<TouchableOpacity style={[styles.button, styles.buttonUpText]}>
                 //<Text style={styles.buttonText}>+</Text>
               //</TouchableOpacity>

少し複雑な記述です。

ここはZeroToFourとFiveToNineの値を繰り返し処理(map)で出力させています。

実際は見本のように描画されていますが10個分の記述を書くとなると非常に長いので繰り返し処理を用いてボタンを作っています。

styleのflexDirection: "row"は、要素を横並びにするスタイルです。
このおかげでかっこいい電卓のレイアウトが出来ましたね。

見本
        <View style={{flexDirection: 'row'}}>
              <TouchableOpacity style={styles.topbutton,styles.buttonNum}>
                <Text style={styles.buttonText}> 0 </Text>
              </TouchableOpacity>
                           .
                           . 1~8省略)
                           .
              <TouchableOpacity style={styles.topbutton,styles.buttonNum}>
                <Text style={styles.buttonText}> 9 </Text>
              </TouchableOpacity>
        </View>

完成版ボタンのスクショ

ようやくここまででレイアウトが整いました。お疲れ様です:pensive::relaxed:

後半では実際にデータをやりとりしたり計算する処理を作っていきます。

前半戦の完全版コードをこちらに置いておきます。うまくいかない方は確認してみましょう!

後編はこちらです!!!

8.数字ボタンを押してその値を描画しよう!

レイアウトが完成したので次は機能を作っていきます。
複雑な部分が多いので頑張っていきましょう:angel:

まずは、数字ボタンが押されたらその数字が描画されるところまでやっていきましょう。

①「ボタンが押されたら値(= 押したボタンの数字)が送信される」という記述を作る

②送信された値を受け取る

③その値を描画する

という流れで作っていきます。

ezgif.com-video-to-gif.gif

①「ボタンが押されたら値が送信される」という記述を作る

早速作っていきましょう。ボタンを作っているコードに追加していきます。

App.js
        <View style={styles.buttonArea}>
          <View style={{flexDirection: 'row'}}>
            {ZeroToFour.map(i => (
                <TouchableOpacity style={styles.topbutton,styles.buttonNum} key={i} onPress={() => this.onNum({i})}>
                  <Text style={styles.buttonText}> {i} </Text>
                </TouchableOpacity>
            ))}
          </View> 

          <View style={{ flexDirection: 'row'}}>
            {FiveToNine.map(i => (
                <TouchableOpacity style={styles.topbutton, styles.buttonNum} key={i} onPress={() => this.onNum({i})} >
                  <Text style={styles.buttonText}> {i} </Text>
                </TouchableOpacity>
            ))}
          </View>

onPress={() => this.onNum({i})}という記述を追加しています。

onPressはjsやJQueryの「click関数」とほぼ同じ意味です。

{i}には繰り返し処理中のindexが入るので、0〜9の値が入りますよね。
これを利用して押されたボタンの数字を取得して送信しているという仕組みです。

②送信された値を受け取る

値の送信は完了したので、その値を処理して受け取る作業をしていきます。

①に追記したonNumは未定義のものなので、定義してあげましょう。

App.js

//import 略

export default class App extends Component {
//ここから
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0
    }
  }
  onNum(input){
    console.log(input); //←デバック用
    this.setState({ leftNum:  input["i"] })
  } 
//ここまで


//render(){
  //return(

onPressイベントから送信された値は、onNumイベントで受け取っています。引数(input)に値が送られてきています。

console.log(input)でどんなものが送られてきたか確認してみましょう!

「7」のボタンを押すと、こんなデータが送信されていることが分かりました。

7のボタン
//inputの中身
Object {
  "i": 7,
}

オブジェクトとして送信されていて、"i"がkey、7がvalueですね。

では、ここから「7」を取得したい場合は以下のようにすれば良いですよね。

input["i"] 
//valueの7が取れる

そして、最後に取得した値を保存します。

保存
this.setState({ leftNum:  input["i"] })

この記述を行うことでstateにあるlaftNumにinput["i"]の値を保存しています。

setStateはstateを変化させたい時に使用するもので「stateにsetする」的なイメージを持ってくれたらOKです。

こうすることで、

    this.state = {
      leftNum: 0
    }

0だったleftNumが

    this.state = {
      leftNum: 7
    }

7に変化します!

③保存した値を描画する

ラストは保存した値を描画する作業です。これは秒です、2秒です。

先ほどまで、「0 ? 0 = 0」としていたところを少し変更します!

1番左の0の部分を{this.state.leftNum}にするだけです。

これで現在のleftNumの値を描画してくれるというわけです。(すごい!)

App.js
//略

  render() {
    return (
      <View>
        <View style={styles.countArea}>
          <Text style={styles.value}>{this.state.leftNum}  ?  0  =  0</Text>
        </View>

9. 2桁以上の入力をできるようにしよう!

ここまででようやく押したボタンの数値が適応されるようになりました。でも、現状だと数字ボタンを押しても値が変更されるだけで1桁しか表示されません。

1桁しか計算できない電卓なんで全然かっこよくないですよね。むしろダサいです。

しかし、僕は2桁以上の入力を出来るようにするやり方に悩みに悩みました、何日も悩みました。1度考えることを辞めた僕はなぜか閃きました。

「1回文字型に変換して足して、数値型に戻せば良いんだ!!!!」

(僕はこのやり方しか思いつかなかったので他にもあれば教えていただきたいです。)

数値型のまま、既存のstateの値と入力された値を足すと普通に足し算されてしまいます。

悪い例
//デフォルトでleftNumに2、input["i"]に4があるとする
this.state.leftNum + input["i"]

2 + 4 = 6になってしまう
成功例
//デフォルトでleftNumに2、input["i"]に4があるとする
String(this.state.leftNum) + String(input["i"])

(文字列の)"2" + (文字列の)"4" = "24"

既存の値と入力された値の両者を文字列に変換して、+してあげると文字列同士の足し算が行われるのでこれを利用しました。

これを使って記述を変更しましょう!

App.js
  onNum(input){
    //console.log(input);
    //this.setState({ leftNum:  input["i"] })
    const num = Number(String(this.state.leftNum) + String(input["i"])) //文字列同士の足し算を行い、最後に数値型に変換
    this.setState({ leftNum:  num }) //leftNumに計算した値をset!
  } 

だんだん完成に近付いてきました。現在70%くらいの完成度です。ラストスパートです!

10.四則演算の選択を可能に!

左側の数値の入力は可能になったので、計算方法の選択が出来るようにしましょう。

数字ボタンの時とやることは同じなのでサクッといきましょう!

App.js
export default class App extends Component {
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0,
      mark: "?",
    }
  }

//~~~略

  render() {
    return (
      <View>
        <View style={styles.countArea}>
          <Text style={styles.value}>{this.state.leftNum}  {this.state.mark}  0  =  0</Text>
        </View>

//~~~略
//~~~略
        <View style={{ flexDirection: 'row'}}>
        //以下4つにonPressを追記
          <TouchableOpacity style={[styles.button, styles.buttonUpText]} onPress={() => this.onMark("plus")}>
            <Text style={styles.buttonText}>+</Text>
          </TouchableOpacity>

          <TouchableOpacity style={[styles.button, styles.buttonMinus]} onPress={() => this.onMark("minus")}>
            <Text style={styles.buttonText}>-</Text>
          </TouchableOpacity>

          <TouchableOpacity style={[styles.button, styles.buttonTimes]} onPress={() => this.onMark("times")}>
            <Text style={styles.buttonText}>x</Text>
          </TouchableOpacity>

          <TouchableOpacity style={[styles.button, styles.buttonDivide]} onPress={() => this.onMark("divide")}>
            <Text style={styles.buttonText}>÷</Text>
          </TouchableOpacity>

やっていることは3つです。

①stateに[+ ー ➗X]を表すための「mark」を追加

②常に今のmarkの値を描画するために{ this.state.mark }を追加

③四則演算のボタンにonPressイベントを追加

この3つは数字ボタンと流れは同じですよね。

この状態でボタンを押してもエラーが出るので、入力したボタンの値を受け取って正常に描画させましょう。

入力してきた値が"plus"ならmarkを「+」に変更というようなイメージです
たくさん条件分岐させていきます。

App.js
onMark(input){
    if (input == "plus"){
      this.setState({ mark: "+"  })
    } else if (input == "minus") {
      this.setState({ mark: "-" })
    } else if (input == "times") {
      this.setState({ mark: "x"} )
    } else if (input == "divide") {
      this.setState({ mark: ""})
    }
  }
}

(↑の➗マークが見えづらくなってます。)

11.右側の数字を入力可能に使用!

現状は左の数値と四則演算のマークの入力は可能になっています。

次は右側の数値の入力ができるようにしましょう!ここも今までと同じ流れで完了です。

App.js
export default class App extends Component {
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0,
      mark: "?",
      rightNum: 0, //←追記
    }
  }
  onNum(input){ //以下追記
    if (this.state.mark == "?"){
      let num = Number(String(this.state.leftNum) + String(input["i"]))
      this.setState({ leftNum: num })
    } else {
      let num = Number(String(this.state.rightNum) + String(input["i"]))
      this.setState({ rightNum: num })
    } 
  }

//~~~~~略

  render() {
    return (
      <View>
        <View style={styles.countArea}> //以下追記
          <Text style={styles.value}>{this.state.leftNum}  {this.state.mark}  {this.state.rightNum}  =  0</Text>
        </View>

ここまでのコードはこちらです。困ったら確認してください:relaxed:

12. 「=」を押したら計算を実行する!

ようやく計算式の入力が出来るようになりました。

「=」が押されたら四則演算のマークに応じて計算が行われるようにしましょう。

まずは「=」ボタンの所に追記しましょう。

App.js
 <TouchableOpacity style={[styles.button, styles.buttonEqual]} onPress={() => this.onEqual()} >
   <Text style={styles.buttonText}> = </Text>
 </TouchableOpacity>

onEqualを定義して、markの値によって条件分岐を行います。

そして、結果の値をsetすれば完了です。

App.js
export default class App extends Component {
  constructor(props){
    super(props);
    this.state = {
      leftNum: 0,
      mark: "?",
      rightNum: 0,
      result: 0 //追記
    }
  }
      :
      :
      :
  onEqual(){
    if (this.state.mark == "+") {
      this.setState( { result: this.state.leftNum + this.state.rightNum })
    } else if (this.state.mark == "-") {
      this.setState( { result: this.state.leftNum - this.state.rightNum})
    } else if (this.state.mark == "x") {
      this.setState( { result: this.state.leftNum * this.state.rightNum})
    } else if (this.state.mark == "") {
      this.setState( { result: this.state.leftNum / this.state.rightNum})
    }
  }

  render() {
    return (
      <View>
        <View style={styles.countArea}>
          <Text style={styles.value}>{this.state.leftNum}  {this.state.mark}  {this.state.rightNum}  =  {this.state.result}</Text> //追記
        </View>

「=」ボタンを押したらonEqualが発火します。

onEqualイベントは、markの値によって条件分岐しています。

例えばmarkが「➗」だったら、leftNumとrightNumを割り算して、resultにsetしています。

これでようやく計算ができるようになりました!!!!!

次でラストです!!!

13.リセットを反映させよう!

最後はリセットボタンです。iPhoneアプリの電卓なら「AC」にあたるところです。

リセットボタンを押したらstateにあるleftNumとrightNumを「0」に、markを「?」に変更します。

App.js
:
:略(onEqual)の下に記述
:
  onReset(){
    this.setState({ leftNum: 0, mark: "?", rightNum:0, result:0 })
  }


:略 リセットボタンに追記

   <TouchableOpacity style={[styles.button, styles.buttonReset]} onPress={() => this.onReset()}>
      <Text style={[styles.buttonResetText]}>リセット</Text>
   </TouchableOpacity>

これで値のリセットが完了しました!

まとめ

ようやく電卓が完成しました。お疲れ様でした。
Reactが初めての方はとても大変でしたよね。

今回のアドベントカレンダーでアウトプットの重要性が身に染みてわかりました。
この記事を書くために調べて、実践して、改善してと非常に良い勉強になりました。

とりあえず本当にお疲れ様でした!
完成版コードの確認をしたい方はこちらです。

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

冬空の中でReactの世界観をちょっとだけ広げたかった

それはただ美しかった。

コンポーネントという呪縛がある一方で、開発者の自由さを守りながら宣言的UIによる美しい構成が可能となった。

数千行に渡るDOMイベント管理の苦行から開放された。

見上げればそこにはReactがあった。

まるでオリオン座のように。

だがしかしBUT

仮想DOMというバーチャルな世界を実現するために、「Hello World」を表示するには現実的にWebpackやRollup等のバンドルが必要になった。

これを私は「コンポーネントの呪縛」と勝手に思っている。(悪気はない。)

初学者だった頃のjQuery使いの私にはしばらく難しかった。

とりあえずこれのようなminimalなスターターを書いて、そっと公式ドキュメントを閉じた。

数年の月日を経て

複雑なステート管理にはReduxが有名に、事実上のデファクトとなった。

やがてHooksが誕生した。

Hooksの登場により再度公式ドキュメントを開き、そしてそれをそっと閉じた。

どうしてもReduxのような管理方法が必要になった

JSerとしての経験値が上がるにつれて、jQuery思想の自分にはやがて限界が訪れた。

コンポーネントである以上基本は親が子を教育するのである。

したがってグローバルなstoreへどのコンポーネントからもアクセスできる機構が必要になったのだ。

dispatchめ。(悪気はない。)

おや...

結局グローバルstoreは親から子へ移譲しているだけではないか。

親から子へデータを渡している?

それは究極の発想ではview template engineに近しいのではなかろうか。

JSならejsやhandlebars、PHPあたりではbladeとかそういった、あの{{ data }}ではなかろうか。

あの古き良き、サーバーから受け取ったデータをビューへ渡す方法ではなかろうか。

Reactを view template engine にできないだろうか

調べたらあった。

https://github.com/reactjs/express-react-views

サーバーからデータを渡す:

// views/index.jsxをレンダリング
res.render('index', { message: 'Hello World' });

するとviews/index.jsxのpropsにデータが伝搬される:

var React = require('react');

function IndexPage(props) {
  return <div>{props.message}</div>;
}

module.exports = IndexPage;

素晴らしいではないか。

と思ったら

It renders static markup and does not support mounting those views on the client.

静的マークアップをレンダリングするだけでReactはマウントされないだと。。。

Reactにする意味はあるのか?(ビューの数千行が数百行くらいにはなるかもしれないが。。。)

JSのイベントはどうする?

CSS-in-JSはどうなる?

どうしてもReactをマウントしたかった

https://github.com/saltyshiomix/react-ssr

$ npm install --save @react-ssr/express
package.json
{
  "scripts": {
    "start": "node server.js"
  }
}
server.js
const express = require('express');
const register = require('@react-ssr/express/register');

const app = express();

(async () => {
  // Reactビューエンジンとして`.jsx`を登録
  await register(app);

  app.get('/', (req, res) => {
    const message = 'Hello World!';
    res.render('index', { message });
  });

  app.listen(3000, () => {
    console.log('> Ready on http://localhost:3000');
  });
})();
views/index.jsx
export default function Index({ message }) {
  return <p>{message}</p>;
}

そう、Reactをそのまま利用できるビューエンジンにしたかった

views/index.jsx
import React from 'react';

const IndexPage = (props) => {
  const [message, setMessage] = React.useState('waiting...');

  const onClick = () => setMessage('This is a react-ssr!');

  return (
    <React.Fragment>
      <button onClick={onClick}>Click Me</button>
      <p>Message from state: {message}</p>
    </React.Fragment>
  );
};

export default IndexPage;

スクリーンショット 2019-11-30 22.12.58.png

↓↓↓

スクリーンショット 2019-11-30 22.13.27.png

想像してみてほしい

server.js
const posts = [
  { id: 1, body: 'This is a first post.' },
  { id: 2, body: 'This is a second post.' },
  { id: 3, body: 'This is a last post.' },
];

app.get('/', (req, res) => {
  res.render('index', { posts });
});

app.get('/posts/:postId', (req, res) => {
  const { postId } = req.params;
  const post = findById(postId);
  res.render('post', { post });
});

この古き良きデータ流しを、

views/index.jsx
import React from 'react';

const IndexPage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default IndexPage;

↓↓↓

http://localhost:3000

スクリーンショット 2019-11-30 22.23.13.png

views/post.jsx
import React from 'react';

const PostPage = ({ post }) => {
  return (
    <React.Fragment>
      <p>{post.body}</p>
    </React.Fragment>
  );
};

export default PostPage;

↓↓↓

http://localhost:3000/posts/1

スクリーンショット 2019-11-30 22.23.55.png

Reactで扱える喜びを...!!!

NEXT.jsとの比較

データの非同期取得

クライアント側のデータ非同期取得は、どちらもReact.useEffect()で非同期関数を処理する必要がある。

NEXT.jsの場合

pages/index.jsx
import React, { useState, useEffect } from 'react';

const HomePage = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    // componentDidMount like
    fn() {
      const res = await fetch('http://localhost:4000/api/posts');
      const posts = await res.json();
      setPosts(posts);
    }
    fn();

    return () => {
      // componentWillUnmount like
    };
  }, []);

  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default HomePage;

しかしこれではSSRされないのでSEO対策にならない。

仕方なく以下のようなNEXT.js wayに従って書き直す。

pages/index.jsx
import React from 'react';

const HomePage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

HomePage.getInitialProps = async () => {
  const res = await fetch('http://localhost:4000/api/posts');
  const posts = await res.json();
  return { posts };
};

export default HomePage;

だいぶすっきりしたものの、非同期データをSSRさせたい場合、このgetInitialPropsという魔法の呪文を唱える必要がある。

react-ssrの場合

すでに紹介したように、view template engineとして機能するため、APIサーバーから取得したデータをそのままビューへ渡せば良い。

そもそも同一サーバーならAPIサーバーを建てる必要もなく、直接DBから取得したデータをビューへ渡せばpropsから取得データへアクセスできる。

server.js
(async () => {
  const posts = await fetchFromDbOrAPIServer();

  app.get('/posts', (req, res) => {
    res.render('posts', { posts });
  });
})();
views/index.jsx
import React from 'react';

const HomePage = ({ posts }) => {
  return (
    <React.Fragment>
      {posts.map((post, index) => {
        return (
          <p key={index}>
            <a href={'/posts/' + post.id}>{post.body}</a>
          </p>
        );
      })}
    </React.Fragment>
  );
};

export default HomePage;

ユーザー認証

NEXT.jsの場合

iaincollins/next-authというライブラリがあるが、これもクライアント側ではgetInitialPropsを利用する。

しかしサーバー側の使い方が、NEXT.js wayなサーバーの書き方をする必要があったり、そもそもドキュメントが充分とは思えないのであまり使う気にはなれなかった。

server.js
const next = require('next')
const nextAuth = require('next-auth')
const nextAuthConfig = require('./next-auth.config')

require('dotenv').load()

const nextApp = next({
  dir: '.',
  dev: (process.env.NODE_ENV === 'development')
})

nextApp.prepare()
.then(async () => {
  const nextAuthOptions = await nextAuthConfig()
  const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)  
  console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
})
.catch(err => {
  console.log('An error occurred, unable to start the server')
  console.log(err)
})

↑を読んでこれから何が起こるのかわかるエスパーはいるのだろうか。

react-ssrの場合

react-ssrはExpressのview template engineとして振る舞うので、古き良きExpress wayなユーザー認証を利用できる。

例えばメールアドレス・パスワードによる認証であればpassportpassport-localを利用する。

ソースコードは長くなるため割愛するが、その分サーバー側で柔軟な調整ができる。

  • ユーザーデータのシリアライズ・デシリアライズ方法の調整
  • sessionの保存方法の調整(たとえばDBにsession情報を保存する等)
  • リダイレクトURLの柔軟な調整(条件分岐等)

サーバー側のバグはクリティカルであることが多い。
「なんでも屋」すぎるライブラリには頼らず、小さくて優秀なライブラリを複数利用しつつ細かい調整は自前で実装するのがベストプラクティスだと筆者は考える。

react-ssrの実装サンプル

examples/with-jsx-emotion

スクリーンショット 2019-12-01 5.25.35.png

examples/with-jsx-material-ui

スクリーンショット 2019-12-01 5.27.23.png

examples/with-jsx-semantic-ui

スクリーンショット 2019-12-01 5.29.42.png

examples/styled-components

スクリーンショット 2019-12-01 5.31.16.png

さいごに

最終的にreact-ssrの宣伝っぽくなってしまってすみません。

ですが古き良きビューエンジンの形でReactを利用したいと思ったことはありませんか?

小規模開発でもわざわざAPIサーバーとクライアントを分けなければならない煩雑さを感じたことはありませんか?

そういった疑問を感じたことがある方にとって、少しでも可能性が広がる記事になれば幸いです。

筆者の稚拙な技術記事であるため、間違い等はコメントにてご指摘ください。

ここまで読んでくださりありがとうございました。

ちなみに私はベテルギウスが好きです。(一番高いところにあるから。)

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

Electron && React のconsole error対応メモ

Electron

Electron Security Warning

Node.js Integration with Remote Content

Electron Security Warning (Node.js Integration with Remote Content) This renderer process has Node.js integration enabled
    and attempted to load remote content from 'http://localhost:3000/'. This
    exposes users of this app to severe security risks.

For more information and help, consult
https://electronjs.org/docs/tutorial/security.
 This warning will not show up
once the app is packaged.

electron上で、http経由のコンテンツを読み込んだ時に発生。
デバック中は、local上でReactを動かしているので発生する。
パッケージ化すれば発生しないエラー。

process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = '1';

を、 electron.js にて追記すれば表示されなくなる。

[参考]
- Security, Native Capabilities, and Your Responsibility | Electron
- Electron Security Warning (Node.js Integration with Remote Content) · Issue #213 · maximegris/angular-electron

Insecure Resources

/.../node_modules/electron/dist/Electron.app/Contents/Resources/electron.asar/renderer/security-warnings.js:95 Electron Security Warning (Insecure Resources) This renderer process loads resources using insecure
  protocols.This exposes users of this app to unnecessary security risks.
  Consider loading the following resources over HTTPS or FTPS. 
 - http://localhost:3000/static/js/bundle.js
- http://localhost:3000/static/js/0.chunk.js
- http://localhost:3000/static/js/main.chunk.js


For more information and help, consult
https://electronjs.org/docs/tutorial/security.
 This warning will not show up
once the app is packaged.

同上

Insecure Content-Security-Policy

Electron Security Warning (Insecure Content-Security-Policy) This renderer process has either no Content Security
    Policy set or a policy with "unsafe-eval" enabled. This exposes users of
    this app to unnecessary security risks.

For more information and help, consult
https://electronjs.org/docs/tutorial/security.
 This warning will not show up
once the app is packaged.

同上

React

semantic ui

table

Warning: validateDOMNesting(...): <th> cannot appear as a child of <thead>.
<Table.Header>
  <Table.Row> <- 抜けてたw
    <Table.HeaderCell>

[参考]
- Table - Semantic UI React

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

【React】コンポーネントの外がクリックされたら閉じるポップアップメニューを実装しよう

ポップアップのカラーピッカーを React で作っているときに、いくつかの問題に直面したのでまとめておこうと思います。
作ったものは下のGIFのようなカラーピッカーです。
Image from Gyazo

この記事では、カラーピッカー機能を取り除いた、下のポップアップメニューの作り方を紹介します。
完成品の CodePen を先に置いておくので、コピペしたい人はこちらをお使いください。

See the Pen React Popup Menu by ジフォ (@G4RDS) on CodePen.

ポップアップメニューの機能

今回作るポップアップメニューには以下のような機能があります。

  • ボタンがクリックされるまでポップアップメニューを表示しない。
  • ポップアップメニュー範囲外をクリックすると、ポップアップメニューを閉じる。
  • ポップアップメニューの内側をクリックされても、ポップアップメニューは閉じない。
  • ポップアップメニューの内側に、ポップアップメニューを閉じるボタンを用意する。

ポップアップメニューを作ろう

Functional Component で作っていきます。
Hooks を使うので、基礎を理解している必要があります。

まずはベースを作ろう

メニューの表示を切り替えるボタンと、メニューを持つコンポーネントを作ります。

// PopupMenu.js
import './PopupMenu.scss'

const PopupMenu = () => {
  return (
    <div className="popup-menu-container">
      <button>
        Toggle Menu
      </button>
      <div className="popoup-menu">
        <div>menu</div>
        <button>
          Close Menu
        </button>
      </div>
    </div> 
  )
}

適当なスタイルを当てておきます。
.popup-menu がレイアウトに影響しないよう、position: absolute を適用します。

// PopupMenu.scss
.popup-menu-container {
  position: relative;
}

.popup-menu {
  position: absolute;
  z-index: 2;

  width: 10rem;
  margin-top: 0.5rem;
  padding: 1rem;
  background: #fff;
  border-radius: 0.5rem;
}

メニューの表示を切り替えよう

React Hook の useState を使って、メニューを表示したり非表示にしたりできるようにします。

// PopupMenu.js
import React, { useState } from 'react'

const PopupMenu = () => {
  const [isShown, setIsShown] = useState(false)

  const handleToggleButtonClick = () => {
    setIsShown(true)
  }

  const handleCloseButtonClick = () => {
    setIsShown(false)
  }

  return (
    <div className="popup-menu-container">
      <button onClick={handleToggleButtonClick}>
        Toggle Menu
      </button>
      <div className={`popup-menu ${isShown ? 'shown' : ''}`}>
        <div>menu</div>
        <button onClick={handleCloseButtonClick}>
          Close Menu
        </button>
      </div>
    </div> 
  )
}
// PopupMenu.scss
.popup-menu {
  transform: scale(0);
  transform-origin: top left;

  transition: transform 0.2s;

  &.shown {
    transform: scale(1);
  }
}

ポップアップメニュー範囲外がクリックされたら閉じるようにしよう

click イベントは通常、クリックされた場所に一番近い要素から遠い要素へ順番にイベントリスナーを実行していきます。
つまり、HTML DOM の一番外側である body に設定されたイベントリスナーは、そこに伝播するまでの要素に設定されたイベントリスナーが伝播を止めない限り、どこがクリックされても必ず実行されます。

とりあえず、どこをクリックしても閉じるようにしよう

この機能を実装するには、document.body にポップアップメニューを閉じるためのイベントリスナーを設定すれば良さそうです。

const handleToggleButtonClick = () => {
  setIsShown(true)

  document.body.addEventListener('click', e => {
    setIsShown(false)
    document.body.removeEventListener('click', documentClickHandler)
  })
}

しかし、このコードには問題があります。

React DOM の onClick などのイベントリスナーは、HTML DOM のイベントリスナーよりも後で呼ばれることが確定しているため、トグルボタンをクリックした時に isShowndocument.body に設定したイベントリスナーによって false になった後、トグルボタンのイベントリスナーが true にしてしまうのです。
また、isShowntrue にした後、document.body にイベントリスナーを再設定してしまいます。

これを解決するには、範囲外イベントリスナーを React DOM イベントリスナーよりも後に実行される場所に設定する必要があります。
それは、document です。
React DOM は document にクリックイベントリスナーを設定することで、どの React DOM 要素がクリックされたのかを判断しています。
同じ要素に複数のリスナーが設定されている場合、先に設定されたリスナーから順番に実行されます。
つまり、React DOM と同じ document にイベントリスナーを設定することで、React DOM のイベントリスナーが処理された後に実行されるようにできるのです。

document.addEventListener('click', e => {
  setIsShown(false)
  document.removeEventListener('click', documentClickHandler)
})

ポップアップメニューの範囲がクリックされたのなら閉じないようにしよう

現状だとメニューを利用することができないので、ポップアップメニューの範囲内かどうかを判定し、範囲内なら閉じる処理を行わないようにしましょう。
これには、二つの実装方法が考えられます。

  • ポップアップメニュー .popup-menu に、これ以上イベントが伝播しないようにするイベントリスナーを設定する。
  • document に設定したイベントリスナーで、クリック対象の要素がポップアップメニュー要素もしくはその子要素であるかどうかを判定する。

簡単なのは前者ですが、React DOM のイベントリスナーで e.stopPropagation() を実行しても、HTML DOM のイベント伝播は止まらないため、機能しません。
面倒ですが、後者の方法で実装します。

ポップアップメニュー要素にアクセスできるようにする

まず、ポップアップメニュー要素を取得するため、useRef フックを使います。

const popupRef = useRef()

// ...

<div
  className={`popup-menu ${isShown ? 'shown' : ''}`}
  ref={popupRef}
>
  ...
</div>

これで、popupRef.current でポップアップメニュー要素にアクセスできるようになりました。

クリックされた要素がポップアップメニュー要素以下であるかを判定する

popupRef.current で取得できる Element のスーパークラスである Node には、引数に指定したノードがこのノードの子孫ノード(自分自身を含む)であるかどうかを判定する Node.contains メソッドがあります。
これを使うことで、クリックされた要素がポップアップメニュー要素以下であるかどうかを判定できます。

const documentClickHandler = e => {
  if (popupRef.current.contains(e.target)) return

  setIsShown(false)
  document.body.removeEventListener('click', documentClickHandler)
}

ポップアップメニュー内に閉じるボタンを用意しよう

一番初めに紹介したカラーピッカーでは、カラーが選択されたらポップアップメニューを閉じるようにしています。
この機能を実装してみましょう。

閉じるときに、document に設定したイベントリスナーを削除するのを忘れないようにしましょう!

const documentClickHandler = e => {
  if (popupRef.current.contains(e.target)) return

  setIsShown(false)
  removeDocumentClickHandler()
}

const removeDocumentClickHandler = () => {    
  document.removeEventListener('click', documentClickHandler)
}

const handleCloseButtonClick = () => {
  setIsShown(false)
  removeDocumentClickHandler()
}

しかし、このコードは正しく動作しません。
removeEventListner は第二引数が同じ参照を指すイベントリスナーを除去するという働きをする関数ですが、documentClickHandler がReact がレンダリングするたびに新しく生成されてしまうため、イベントリスナーを除去できずに残り続けてしまいます。
これを解決するために、useRef を使った documentClickHandler インスタンス変数を用意し、useEffect で最初のレンダリング後に関数をセットするようにします。

const PopupMenu = () => {

  // ...

  const documentClickHandler = useRef()

  useEffect(() => {
    documentClickHandler.current = e => {
      // ...
    }
  }, [])

  const removeDocumentClickHandler = () => {
    document.removeEventListener('click', documentClickHandler.current)
  }

  const handleToggleButtonClick = () => {
    // ...
    document.addEventListener('click', documentClickHandler.current)
  }

正しく動作するようになりましたね!

? 完成! お疲れ様でした

React DOM は HTML DOM の要素にセットしているイベントリスナーはダミーで、実際は document にセットしたイベントリスナーが処理していたとは、まったく気づきませんでした。

今回はメニューの内部も含めて一つのコンポーネントにしましたが、実際に運用する React プロジェクトでは機能だけを提供するコンポーネントを用意して、再利用できるようにすると良いのかなと思います。

最後に宣伝をさせてください!

今年の春ごろに、毎日やりたいことを習慣づけるアプリ「コツコツ忍者」を作りました!
毎日開発する!とか、毎週土日はランニングする!とかの目標を作り、記録することでモチベーションを保つアプリです!
Nuxt.js で作ったもので、何を使っているのかなどを書いた記事もありますので、ぜひそちらもご覧ください。

また、英語リスニング学習アプリを Flutter で開発中です。
ある程度までできたら、事前登録サイトを用意しようと思っていますので、Twitterをフォローしていただけると幸いです!

最後までご覧いただきありがとうございました!

参考文献

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

よく使う画面遷移のCSS

iosにありがちなモーダルの遷移とプッシュの遷移のサンプル

デモ
https://nakadoribooks.github.io/modal_and_push/

page.gif

使うもの

create-react-app
styled-components

コード

  • styleの定義
  • Reactの実装

スタイル

アニメーションはだいたいこのくらい

duration: 600
ease: 'cubic-bezier(.17,.71,0,1)'

styles.js
import React from 'react';
import styled from 'styled-components';

const sizes = {
  headerHeight: 72
}

// だいたいこのくらい
const animation = {
  page: {
    duration: 600,
    ease: 'cubic-bezier(.17,.71,0,1)'
  },
}
animation.page.transition = `transform ${animation.page.duration}ms ${animation.page.ease}, opacity ${animation.page.duration}ms ${animation.page.ease}`

const colors = {
  primary: "#3880ff",
  secondary: "#0cd1e8",
  border: "#eeeeee",
  white: "#ffffff",
  black: "#000000",
}

const Header = styled.div`
  width: 100%;
  padding:0px 20px;
  position:absolute;
  height: ${sizes.headerHeight}px;
  border-bottom:1px solid ${colors.border};
  background-color:${colors.white};
  padding-top:1px;
  justify-content:space-between;
  display:flex;
  align-items:center;
  box-shadow: 0 2px 2px rgba(200,200,200,.1);

  h1{
    font-size:20px;
  }
`

const HeaderButton = styled.a`
  display:inline-block;
  height:40px;
  font-size:14px;
  line-height:38px;
  border: 1px solid ${colors.primary};
  border-radius:5px;
  color:${colors.primary};
  padding:0px 20px;
  min-width: 100px;
  text-align:center;
  cursor:pointer;
  :hover{
    background-color:${colors.primary};
    color:${colors.white};
  }
`
const HeaderSpace = styled.div`
  width:100px;
`

const RootView = styled.div``

const PageWrapper = styled.div`
  position:fixed;
  top:0px;left:0px;right:0px;bottom:0px;
`

// 後ろにある半透明の黒
const BehindLayer = styled.div`
  background-color:${colors.black};
  opacity: 0;
  position:absolute;
  top:0px;left:0px;right:0px;bottom:0px;
  transition: opacity ${animation.page.duration}ms ease;
  ${(props)=>props.showed && `
    opacity:0.3;
  `}
`

const PageContainer = styled.div`
  height:100%;
  position:relative;
  top:0px;
  transition: ${animation.page.transition};
`

const RootContainer = styled(PageContainer)`

  /* 上にモーダルきた時に少しちっさくなるやつ */
  ${props=>props.modaling && `
    transform: scale(0.99)
  `}

  /* プッシュされた時につこし左に行くやつ */
  ${props=>props.pushing && `
    transform: translateX(-10vh)
  `}
`

const ModalContainer = styled(PageContainer)`
  transform: translateY(100vh);
  ${props=>props.showed && `
    transform: translateY(0vh);
  `}
`

const PushContainer = styled(PageContainer)`
  transform: translateX(50vh);
  opacity: 0;
  ${props=>props.showed && `
    opacity: 1.0;
    transform: translateX(0vh);
  `}
`

const PageContents = styled.div`
  height:calc(100vh - ${sizes.headerHeight}px);
  position:relative;
  top:${sizes.headerHeight}px;
  overflow-y:scroll;
  background-color:${colors.white};
`

const LinkButton = styled.p`
  text-align:center;
  a{
    display:inline-block;
    padding:10px 20px;
    border-radius: 10px;
    border: 1px solid ${colors.border};
    cursor:pointer;
    transition: background-color 300ms ease, transform 300ms ease;
    :hover{
      opacity: 0.7;
    }
  } 

  /* フォーカス残るよう */
  ${props=>props.focus && `
    a{
      background-color:${colors.border};
      transofrm:scale(0.99);
    }
  `}
`

const CenterContainer = styled.div`
  width:100%;
  height:100%;
  display: flex;
  justify-content: center;
  align-items: center;
`

export {
    animation, Header,  HeaderButton, HeaderSpace, RootView, PageWrapper, BehindLayer,
    RootContainer, ModalContainer, PushContainer , PageContents, LinkButton, CenterContainer
}

React 実装

遷移先は didMountで少し遅らせてアニメーション表示

setTimeout(()=>{
  this.setState({showed: true})
}, 10)

遷移元は アニメーション中のフラグとmodal/pushを表示する用のフラグ二つづつもつ。

const {modaling, pushing, showingModal, showingPush} = this.state

App.js
import React from 'react';

import {
  animation, Header,  HeaderButton, HeaderSpace, RootView, PageWrapper, BehindLayer,
  RootContainer, ModalContainer, PushContainer , PageContents, LinkButton, CenterContainer
} from './styles'

class ModalPage extends React.Component{

  state = { showed: false }

  componentDidMount(){
    // アニメーションで表示
    setTimeout(()=>{
      this.setState({showed: true})
    }, 10)
  }

  render = () =>{
    const {showed} = this.state

      return <PageWrapper>
        <BehindLayer showed={showed} />
      <ModalContainer showed={showed}>
        <Header>
          <HeaderButton onClick={this.handleClickClose}>閉じる</HeaderButton>
            <h1>モーダル</h1>
          <HeaderSpace />
        </Header>
        <PageContents>
          <CenterContainer>
            <h1>モーダル</h1>
          </CenterContainer>
          </PageContents>
        </ModalContainer>
      </PageWrapper>
  }

  handleClickClose = (event) => {
    this.setState({showed: false})
    this.props.onClose()  
  }
}

class PushPage extends React.Component{

  state = { showed: false }

  componentDidMount(){
    // アニメーションで表示
    setTimeout(()=>{
      this.setState({showed: true})
    }, 10)
  }

  render = () =>{
    const {showed} = this.state

      return <PageWrapper>
        <BehindLayer showed={showed} />
      <PushContainer showed={showed}>
        <Header>
          <HeaderButton onClick={this.handleClickClose}>戻る</HeaderButton>
            <h1>プッシュ</h1>
          <HeaderSpace />
        </Header>
        <PageContents>
          <CenterContainer>
            <h1>プッシュ</h1>
          </CenterContainer>
          </PageContents>
        </PushContainer>
      </PageWrapper>
  }

  handleClickClose = (event) => {
    this.setState({showed: false})
    this.props.onBack()  
  }
}

class App extends React.Component{

  state = {
    pushing: false,
    modaling: false,
    showingModal: false, 
    showingPush: false 
  }

  render = () => {

    const {modaling, pushing, showingModal, showingPush} = this.state

    return <RootView>
      <PageWrapper>
        <RootContainer modaling={modaling} pushing={pushing}>
          <Header><div /><h1>ページ遷移サンプル</h1><div /></Header>
          <PageContents>
            <CenterContainer>
              <div>
                {/* 戻ってきた時に分かりやすいようにフォーカスを残しておく */}
                <LinkButton focus={showingModal}><a onClick={this.handleClickModal}>モーダル</a></LinkButton>
                <LinkButton focus={showingPush}><a onClick={this.handleClickPush}>プッシュ</a></LinkButton>
              </div>
            </CenterContainer>
          </PageContents>
        </RootContainer>
      </PageWrapper>

      {showingModal && <ModalPage onClose={this.handleCloseModal} />}
      {showingPush && <PushPage onBack={this.handleBackPush} />}

    </RootView>
  }

  handleClickModal = (event) => {
    this.setState({modaling: true, showingModal:true})
  }

  handleClickPush = (event) => {
    this.setState({pushing: true, showingPush: true})
  }

  handleCloseModal = (event) => {
    this.setState({
      modaling: false
    })

    setTimeout(()=>{
      this.setState({showingModal: false})
    }, animation.page.duration)
  }

  handleBackPush = (event) => {
    this.setState({pushing: false})
    setTimeout(()=>{
      this.setState({showingPush: false})
    }, animation.page.duration)
  }
}

export default App;

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

Redux ExampleのTodo ListをはじめからていねいにをもういちどTypescriptとImmerで

概要

immerよいと聞いたので、試してみようと思った。
以前、Redux ExampleのTodo Listをはじめからていねいに(1)
参考にした記事でやろうと思ったが、時間が経っていて古いので改めて環境作成から行った。
ハマった。

環境

バージョン

  • Windows 10 Home
  • Vagrant 2.2.6
  • virtualbox 6.0.14
  • Ubuntu 18.04 LTS (Bionic Beaver)
  • Docker version 19.03.2, build 6a30dfc
  • docker-compose version 1.24.1, build 4667896b

ライブラリのバージョン

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "immer": "^5.0.0",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-redux": "^7.1.3",
    "redux": "^4.0.4"
  },
  "devDependencies": {
    "@types/jest": "^24.0.23",
    "@types/node": "^12.12.14",
    "@types/react": "^16.9.13",
    "@types/react-dom": "^16.9.4",
    "@types/react-redux": "^7.1.5",
    "@types/redux": "^3.6.0",
    "react-scripts": "^3.2.0",
    "typescript": "^3.7.2"
  }
}

ディレクトリ構成(Hello world 時)

app
  - tsconfig.json  # ビルドツールで利用するTypescript設定
  - package.json   # ライブラリを記載・また、起動スクリプトなどを記載
  - yarn.lock      # npmでインストールするライブラリのバージョン固定用
  - src
    - index.tsx  # エントリーポイント
    - components # Reactコンポーネント
      - App.tsx
    - react-app-env.d.ts # TSコンパイラへのファイル間の依存関係の宣言
  - public # 開発サーバのベースとなるフォルダ
    - index.html # 開発サーバのホーム。

  + dist   # ビルドされたファイルの格納先
  - docker # dockerファイルをまとめたフォルダ
    - docker-compose.yml # コンテナ起動時設定ファイル
    - node
      - Dockerfile # コンテナ作成ファイル
  - bin # docker-composeの操作をシェル化
    - start.sh # 開発サーバの起動
    - build.sh # dist内にjsファイルをビルド

ソースのもとについて

以下で構築したフォルダ構成を参考に作成

npm install create-react-app
npx create-react-app my-app --typescript

設定ファイル

ie のサポートはしない予定なので設定ファイルを書換。(*)(**)

tsconfig.json
{
   "compilerOptions": {
-    "target": "es5",
-    "allowJs": true,
-    "skipLibCheck": true,
-    "esModuleInterop": true,
+    "target": "es6",
+    "allowJs": false,
+    "skipLibCheck": false,
+    "esModuleInterop": false,
     "allowSyntheticDefaultImports": true,

docker 設定

docker/docker-compose.yml
# lesson_buildtool_react_tsというコンテナ名で作成
my_react_ts:
  # Dockerfileビルド
  build: ./node
  # ディレクトリを共有する。
  volumes:
    # ビルドするソースファイル
    - ../src:/app/src
    # ビルドファイルの出力先
    - ../dist:/app/build
    # 開発用サーバのホームページに使用するhtml用ディレクトリ
    - ../public:/app/public
    # package.json上書き
    - ../package.json:/app/package.json
    # typescriptの設定ファイル
    - ../tsconfig.json:/app/tsconfig.json
  # ホストのポート8080をコンテナのポート3000にポートフォワーディング
  ports:
    - 8080:3000 # ホスト:コンテナでポート指定
  environment:
    - CHOKIDAR_USEPOLLING=true # デフォルトの設定の場合、vagrantだとファイルの変更を検知できない。ホットリロードのためにpollingが必要。
  # docker-compose run を行ったときにコンテナ上で下のコマンドを行う
  command: [yarn, start]

開発サーバ用 HTML

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

1. Hello World

create-react-app で作成された状態のソースは余分なものも含まれているので、まずはシンプルにする。
整理前のソース

ソース

src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";

// reactのコンポーネントを#root以下に作成する
ReactDOM.render(<App />, document.getElementById("root"));
src/components/App.tsx
import React from "react";

// React.FC は React.FunctionComponent の短縮形
// @types/reactとlib.dom.d.tsで型が衝突することがあるため、慣習としてReactの型はnamed import({FC} from 'react'みたいなやつ)を避ける
const App: React.FC = () => {
  return <div> Hello World!!! </div>;
};

export default App;

整理後のソース

実行

開発用サーバを起動。

./bin/start.sh

ブラウザでアクセスして確認。

http://192.168.50.10:8080/

2. actionCreator で発行した action を reducer に渡して store の state を更新する

参考*をもとに作成。
Flux Standard Action という考えかたがあるよう(*)なので、type(必須) と payload(オプション) の組にしてみる。サンプルから少し逸脱。

Acitions

src/actions/index.ts
// TypeScript3.4で導入された const assertion を利用することで各定数がstringではなく、その文字列の型として定義される
const ADD_TODO = "ADD_TODO" as const;

let nextTodoId = 0;

// actionを発行する関数
export const addTodo = (text: string) => {
  // actionはtypeを持つオブジェクト
  // この場合、アクションタイプはADD_TODO
  // データはpayloadとなる。
  return {
    type: ADD_TODO,
    payload: {
      id: nextTodoId++,
      text
    }
  };
};

// TypeScript2.8で導入されたReturnTypeで型をかえす
export type AddTodoAction = ReturnType<typeof addTodo>;

Reducers

immer を使ってみた(*)。
produce 関数で、普通(ミュータブル)に書いたものが、イミュータブルなオブジェクトとして返されることとなる(*)。

src/reducers/index.ts
import produce, { Draft } from "immer";
import { AddTodoAction } from "../actions";

export class TodoState {
  constructor(public id: number, public text: string) {}
}

const todo = produce((draft: Draft<TodoState>, action: AddTodoAction) => {
  switch (action.type) {
    case "ADD_TODO":
      const {
        payload: { id, text }
      } = action;
      draft.id = id;
      draft.text = text;
      return draft;

    default:
      return draft;
  }
});

export default todo;

この状態だと、以下のエラーが出てハマった。

my_react_ts_1  | Failed to compile.
my_react_ts_1  |
my_react_ts_1  | /app/src/index.tsx
my_react_ts_1  | TypeScript error in /app/src/index.tsx(7,27):
my_react_ts_1  | No overload matches this call.
my_react_ts_1  |   Overload 1 of 2, '(reducer: Reducer<unknown, Action<any>>, enhancer?: StoreEnhancer<unknown, unknown> | undefined): Store<unknown, Action<any>>', gave the following error.
my_react_ts_1  |     Argument of type '<Base extends { readonly id: number; readonly text: string; }>(base: Base, action: { type: "ADD_TODO"; payload: { id: number; text: string; }; }) => Base' is not assignable to parameter of type 'Reducer<unknown, Action<any>>'.
my_react_ts_1  |       Types of parameters 'base' and 'state' are incompatible.
my_react_ts_1  |         Type 'unknown' is not assignable to type '{ readonly id: number; readonly text: string; }'.
my_react_ts_1  |   Overload 2 of 2, '(reducer: Reducer<unknown, Action<any>>, preloadedState?: DeepPartial<unknown> | undefined, enhancer?: StoreEnhancer<unknown, {}> |
undefined): Store<...>', gave the following error.
my_react_ts_1  |     Argument of type '<Base extends { readonly id: number; readonly text: string; }>(base: Base, action: { type: "ADD_TODO"; payload: { id: number; text: string; }; }) => Base' is not assignable to parameter of type 'Reducer<unknown, Action<any>>'.  TS2769
my_react_ts_1  |
my_react_ts_1  |      5 | import todo from "./reducers";
my_react_ts_1  |      6 | import { addTodo } from "./actions";
my_react_ts_1  |   >  7 | const store = createStore(todo);

tsconfig の設定を"strict": false,にすると動く。型の問題だけっぽい。
Draft<TodoState> が違っていた模様。以下のようにしたら動いた。

src/reducers/index.ts
import produce from "immer";
import { AddTodoAction } from "../actions";
import { Reducer } from "redux";
export class TodoState {
  constructor(public id: number, public text: string) {}
}

export const initialState = () => new TodoState(0, "");

const todo: Reducer<TodoState, AddTodoAction> = produce((draft, action) => {
  switch (action.type) {
    // actionTypeがADD_TODOのとき、
    // 新しいTodoStateを返す
    case "ADD_TODO":
      // es6の分割代入でpayloadからidとtextを取り出す
      const {
        payload: { id, text }
      } = action;
      draft.id = id;
      draft.text = text;
      return draft;
    // それ以外のときはstateを変化させない。何もしないproduceは、元の状態を返す。
    // default:
    //   return draft;
  }
});

export default todo;
src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App";
import { createStore } from "redux";
import todo, { initialState } from "./reducers";
import { addTodo } from "./actions";

// createStoreの引数が1つだと初期値がなくてエラーとなる
const store = createStore(todo, initialState());
store.dispatch(addTodo("Hello World!"));
console.log(store.getState());
ReactDOM.render(<App />, document.getElementById("root"));

この時点のソース

2.5 store, reducer の整理

reducer

  • 名前が分かりにくいので todo から reducer にリファクタリング
  • todoList を作るようの interface を定義
  • todoList を作る用の reducer に変更
  • 今後 reducer は増えていくので、集約が出来るように準備をしておく。
  • store フォルダに格納
src/stores/todos/index.ts
import produce from "immer";
import { AddTodoAction } from "../../actions";
import { Reducer } from "redux";

interface Todo {
  id: number;
  text: string;
}

interface State {
  todos: Todo[];
}

export function initialState(): State {
  return { todos: [] };
}

export const reducer: Reducer<State, AddTodoAction> = produce(
  (draft, action) => {
    switch (action.type) {
      case "ADD_TODO":
        const { payload } = action;
        draft.todos.push(payload);
        return draft;
    }
  }
);
  • combineReducers を使って集約する。
  • 今回は1つだけなのであまり意味はない。
src/store/reducer.ts
import { combineReducers } from "redux";
import * as Todos from "./todos";
export function initialState() {
  return {
    todos: Todos.initialState()
  };
}
export const reducer = combineReducers({ todos: Todos.reducer });

store

src/index.tsx にあった createStore を関数に切り出してフォルダを分ける。

src/store/index.ts
import { createStore, Store } from "redux";
import { initialState, reducer } from "./reducers";
export type StoreState = ReturnType<typeof initialState>;
export type ReduxStoreInstance = Store<StoreState>;

export function initStore(state = initialState()) {
  // createStoreの引数が1つだと初期値がなくてエラーとなる
  return createStore(reducer, state);
}

3. store で保持した state を View で表示する

components

src/components/Todo.tsx
import React from "react";

const Todo: React.FC<{ text: string }> = ({ text }) => {
  return <li>{text}</li>;
};

export default Todo;
src/components/TodoList.tsx
import React from "react";
import Todo from "./Todo";
import { State } from "../store/todos";

const TodoList: React.FC<State> = props => {
  return (
    <ul>
      {props.todos.map(todo => (
        <Todo key={todo.id} {...todo} />
      ))}
    </ul>
  );
};

export default TodoList;

container

src/container/VisibleTodoList.tsx
import { connect } from "react-redux";
import TodoList from "../components/TodoList";
import { StoreState } from "../store";

const mapStateToProps = (store: StoreState) => {
  return { todos: store.todos.todos };
};

const VisibleTodoList = connect(mapStateToProps)(TodoList);

export default VisibleTodoList;

App

src/components/App.tsx
import React from "react";
import VisibleTodoList from "../containers/VisibleTodoList";

const App: React.FC = () => {
  return (
    <div>
      <VisibleTodoList />
    </div>
  );
};

export default App;

index

index.tsx

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./components/App";
import { addTodo } from "./actions";
import { initStore } from "./store";

const store = initStore();

store.dispatch(addTodo("Hello World!"));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

ここまでで、画面にリストが出ることを確認できる。

この時点のソース

いつのまにか以下のエラーがでるようになっていた。。

Reducer "todos" returned undefined during initialization.
If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. If you don't want to set a value for this reducer, you can use null instead of undefined.

以下のように reducer を直したらエラーは発生しなくなった。*

src/stores/todos/index.ts
export const reducer: Reducer<State, AddTodoAction> = produce(
-  (draft, action) => {
+  (draft = initialState(), action) => {
    switch (action.type) {
      case "ADD_TODO":
        const { payload } = action;
        draft.todos.push(payload);
        return draft;
      default:
        return draft;
    }
  }
);

4.フォームから todo を追加

containers/AddTodo.tsx
import React from "react";
import { Dispatch } from "redux";
import { connect } from "react-redux";
import { addTodo } from "../actions";

const mapDispatchToProps = (dispatch: Dispatch) => ({
  handleClick: (text: string) => dispatch(addTodo(text))
});

let AddTodo: React.FC<any> = ({ handleClick }) => {
  let input: HTMLInputElement;

  return (
    <div>
      <input
        ref={node => {
          input = node!; // nodeがnullはありえないので、!でnullでないことを示す
        }}
      />
      <button
        onClick={() => {
          handleClick(input.value);
          input.value = "";
        }}
      >
        Add Todo
      </button>
    </div>
  );
};

AddTodo = connect(null, mapDispatchToProps)(AddTodo);

export default AddTodo;
src/comonents/App.tsx
import React from "react";
import VisibleTodoList from "../containers/VisibleTodoList";
+import AddTodo from "../containers/AddTodo";
const App: React.FC = () => {
  return (
    <div>
+      <AddTodo />
      <VisibleTodoList />
    </div>
  );
};

export default App;

この時点のソース

参考

【Typescript×React】tsconfig.json の設定項目を詳しく紹介

tsconfig 日本語訳(3.03)
[TypeScript] create-react-app で始めるだいたいストレスフリーな開発環境の構築 2
Redux 開発で絶対使うべき Redux DevTools Extension 解説
Redux Example の Todo List をはじめからていねいにを Typescript で(1)
Immerjs で Redux 周りをスッキリさせたい
create-react-app で作った雛形のコードが Service Worker で何をしているのか
React + TypeScript の ESLint ルールをカスタマイズしたり、Airbnb のやつを導入するぞ。
トリプルスラッシュ・ディレクティブ
関東最速で React+Redux+TypeScript なアプリの開発環境を作る
redux を typescript で使うならこれを使うしかない。(typescript-fsa がすごい)
typescript-fsa に頼らない React × Redux
TypeScript で Redux の Reducer 部分を型安全かつスッキリ書く
TypeScript 2.4+における Redux Action
Flux Standard Action(FSA)の説明
Redux の createStore()の処理を追う(Middleware 有りの場合)
React ビギナーズガイドを typescript で勉強し直してわかったこと ②【propTypes の必要性について】
React を TypeScript で書く 3: React 編
react-redux-typescript-guide
React を TypeScript で書ける環境で、Redux の Tutorial をしてみる
React (TypeScript): ベストプラクティス
エラー「Reducer returned undefined during initialization」(React/Redux)
Redux をソースコードから理解する その 1

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