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

Reactカスタムフック入門 ! (ドラッグでDOMを動かすカスタムフックを作る)

カスタムフック入門 !

この記事の内容

前半 : Hooksの紹介
後半 : 以下のような、ドラッグでDOMを動かすカスタムフックの解説
objectMove.gif

Hooks!!

Hooksとは?

Hooksとは、Reactに最近追加されたAPI群です。
Hooksを使えば、関数型コンポーネントでも従来のクラス型コンポーネントとほぼ同等(筆者は困ったことないです)の処理ができるようになります。
というか、より簡潔にコードが書けるしめちゃくちゃ使いやすいので、正直もうクラス型には戻れないです。
Good bye, クラス型コンポーネント。

クラス型は勉強しなくていい?

クラス型も勉強した方がいいです。
なぜなら、既存のコードの大多数はクラス型で書かれているから。
他人のコードを参考にしたり、ソースコードを読む時に必要になるでしょう。

Hooksのメリットは「いい感じの疎結合さ」

Hooksを使った関数型コンポーネントの利点は本当にたくさんあります。
粒度を気にせずに雑多に少し列挙すると

  • 渡ってくるpropsを明示できる
  • thisに束縛されない(bindを使う必要がない)
  • this.state.hogeって書かなくていい(hogeだけ)
  • カスタムフックを作ることで、ロジックをコンポーネントから分離できる
  • テストが書きやすい
  • 状態と状態のバインディングが簡単にできる

これらをまとめると、Hooksのメリットは「いい感じの疎結合さ」だと思います。
疎結合なので変数やイベントがthisに束縛されませんし、ロジックを分離でき、テストもしやすい。
また、単に疎結合が進むだけでなく、ロジックなどはまとめられたり、状態と状態のバインディングがしやすくなったりと、扱いやすい塊にいい感じに切り分けられます。

Hooksの例

useStateがHooksの代表例です。
クラス型のthis.stateに替わるものです。
以下をご覧ください。

hooksState.js
import { useState } from "react"

const component=()=>{
    const [ hoge, setHoge ] = useState("初期値")
}

useStateの引数が初期値で、返り値の一つ目が状態変数(クラス型で言うthis.state.hoge)、二つ目がそのセッター関数です。
この返り値の名前は自分で決められますが、セッターはset{大文字から始めた状態変数名}にすることが多いです。

コードを見ると、なんとなくコンポーネントから分離している感じがあると思います。

詳しくは公式ドキュメント参照のこと。

useEffectがすごい

もう一つご紹介。
HooksAPIの1つにuseEffectという関数があります。
副作用を扱うAPIなんですが、これが本当に便利。

例えば、あるコンポーネントがAとBっていう状態変数をもっていた時に、Aが変わった時にBも変えたいってときありますよね。
そういうとき、クラス型だと、Aを変える処理の後に毎回Bを変える処理を書く必要がある。
AやBの値で分岐があったらそれも書く必要がある。

それを、useEffectだと一箇所に書くだけでいいのです。

effect.js
import { useEffect } from "react"

useEffect(()=>{
    // Bに関する処理
},[A])

こんな感じで、useEffectの
第一引数に行いたい処理を、
第二引数には、変更をその処理のトリガーにする変数を
配列で入れます(複数指定可能)。

Reactでは状態変数変化に伴うリレンダリングによって状態とグラフィックのバインディングを実現しています。
HooksのuseEffectを使えば、状態と状態のバインディング(状態Aが変われば状態Bも変わる)も簡単に実装できるのです。

ライフサイクルの代替とだけ思われがちですが、本質は状態と状態のバインディングだと思います。

カスタムフックって何?

さて、Hooksの良さも伝わってきたところで、カスタムフックの説明に入ります。
カスタムフックとは、Hooksを使ったユーザーオリジナルの新しい関数を指します。
ただの関数といえばそれまでですが、HooksAPIを使うことで、差分レンダリングや先述の状態-状態バインディングなどを利用することができます。

DOMをドラッグで動かせるカスタムフックを作る!

さて、ではDOMをドラッグで動かせるようなカスタムフックを作りたいと思います。
クラス型だと、クラスとロジックが密結合して再利用性に乏しい実装になってしまいがちですが、Hooksなら分離できます!

このカスタムフックを構成するHooks

使うHooksは、先述のuseState, useEffectです。

このカスタムフックの設計

  1. まず、対象となるDOM上でMouseDownされた時に、その時のマウス座標(初期マウス座標)を保存します。
  2. 次に、MouseMoveされた時、そのときのマウスがどれくらい先ほど保存した初期マウス座標からズレているかを計算します。
  3. そのベクトルを、DOMにstyleのtransform3dに入れて渡してあげます
  4. MouseUpされたら、初期マウス座標をリセット

少し簡略化して描いたので、後のコードを見て実装を確認してください。

コード

まず完成コードをお見せします。
まずは、objectのカスタムフック(コードは後述)を適用するところから。

object.js
// ごめんなさいカスタムフック外で新しいHooks、useRef使います
import React, { useRef } from 'react';
import useMoveObject from "./useMoveObject"

function App() {

    // 動かしたいDOMに参照を張る currentには最新のDOMが入る 初期値はnullにしてある
    const boxRef = useRef(null)
    const mouseEventAndStyle = useMoveObject(boxRef.current)

    return (
        <div>
            <div {...mouseEventAndStyle}>
                <div style={{backgroundColor:"red", width:30, height:30}} ref={boxRef}>
                </div>
            </div>
        </div>
    );
}

export default App;

useObjectMoveがカスタムフックで、その返り値(eventTriggerAndStyle)をobject.jsで(仮想)DOMの中に展開しています(useRefを使ってboxRef.currentにはobjectのDOMを入れています)。

後述のuseObjectMove.jsを見てもらえればわかる通り、useObjectMoveは返り値として、onMouseDownとonMouseUp、そしてstyleをkeyに持つオブジェクトを返しています。

このオブジェクトをDOMの中で展開しているので、DOMにそれらをインラインで書き込んでいるのと同じです。
例えば、下記のsample1.jsとsample2.jsは結果としてDOMに同じ処理をしています。

sample1.js
const returnObject = {
    onMouseDown={hoge} 
    onMouseUp={hogege} 
    style={hogegege}
}

<div {...returnObject}>
</div>

sample2.js
<div onMouseDown={hoge} onMouseUp={hogege} style={hogegege}>
</div>

続いて、ここで使われているカスタムフックuseObjectMoveはこんな感じ。

useMoveObject.js
import { useState, useEffect } from "react"

function useMoveObject(object) {

    // オブジェクトをオリジナルの座標から動かすベクトルを表す変数 XとYをkey、動かすピクセル数をvalueに取る
    const [ objectMoveVector, setObjectMoveVector ] = useState( {X: 0, Y: 0} )

    // ドラッグし始めた(MouseDownした)時のマウス座標 MouseDown時に保存し、MouseUp時にnullを入れてリセット
    const [ mouseMoveStartCoordinates, setMouseMoveStartCoordinates ] = useState( null )


    useEffect(()=>{

        if( !object )return // DOMのマウント前はobjectがnullなので
        if( !mouseMoveStartCoordinates ) return // ドラッグし始めてないので

        // ドラッグし始めたならMouseMoveをイベントリスナーに登録し、onMouseMoveでobjectを動かす用のベクトルを計算する
        document.addEventListener('mousemove', onMouseMove)

        // MouseUpしたらドラッグ開始座標をリセット かつ MouseMoveのイベントリスナーも解除
        document.addEventListener('mouseup', ()=>{ 
            setMouseMoveStartCoordinates( null ) 
            document.removeEventListener('mousemove', onMouseMove)
        })

    }, [ mouseMoveStartCoordinates ])



    const onMouseMove=(e)=>{

        // ドラッグ開始地点から現在のマウスまでのベクトルを計算し、最後にドラッグ終了した時のobjectが動いたベクトルに加算
        // イベントリスナーに登録したこの関数はクロージャなので、objectMoveVectorは、この中では常に最後にドラッグ終了した時のもの
        let vector = { 
            X : e.clientX - mouseMoveStartCoordinates.X + objectMoveVector.X, 
            Y : e.clientY - mouseMoveStartCoordinates.Y + objectMoveVector.Y
        }
        setObjectMoveVector(vector)
    }


    return {

        // MouseDownでドラッグ開始時の座標を保存
        onMouseDown : (e)=>setMouseMoveStartCoordinates( { X : e.clientX, Y : e.clientY } ),

        // MouseUp時にマウス位置をリセット
        onMouseUp : ()=>setMouseMoveStartCoordinates(null),

        // 動かすべきベクトルをstyleとして返す
        style: { transform:`translate3d(${objectMoveVector.X}px, ${objectMoveVector.Y}px, 0)` }
    }
}

export default useMoveObject


DOMが欲しいのは、マウスイベントと、その結果である自身を動かすためのstyleであって、ドラッグ開始地点の座標などは不必要です。
そういった媒介となるだけの変数をカスタムフック内部に隠蔽できるので、コードがきれいになり、再利用性も高まります。
クラス型だと、こういった媒介となる変数もコンポーネントに持たせる必要がありました。

ポイント

ポイントは、先述の返り値の展開の他だと、クロージャの部分ですね。
イベントリスナーで登録した関数はクロージャになっていて、中で使う変数の値は、イベントリスナーの登録をした時の値です。
たとえその変数が更新されていようと、古い値が参照されます。
なので、イベントリスナを使う時は基本useRefを使って最新の値を参照します。

しかし今回は、マウスをドラッグし始めた座標を、クロージャの中で固定にしたかったので、そのままにしました。

objectMove.png

まとめ

と言うわけで、Hooksでロジックを分離できるしそもそも書きやすいし最高!っていう記事でした!
ちなみに、クラスコンポーネントとHooksを使った関数コンポーネントは一緒に使うことができます(コンポーネント指向的に当然と言えば当然ですが)。
既存のコードも少しずつ移行できるのがいいですね。

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

FirebaseとReactを使って学祭の模擬店用にカウンターアプリを作った話

去年の秋に大学の学祭で、クラスで模擬店を出したのですが、その売上をリアルタイムで把握できるような簡易Webアプリを作りました。アウトプットの練習も兼ねて、それについて書こうかと。

自己紹介

もうすぐB2になろうとしている、横浜在住の大学生です。
中2の頃Javaを学び始めて、それ以降Android・iOSのアプリやPythonでのDNNに手を出しつつ、コードをいじったりいじらなかったり。
今の所一番好きな言語はKotlinです。これ作るまでWeb全然触っていなかった。あとC言語系も全然なのでそのうち触りたい。

目的

アプリの目的

模擬店での売上をリアルタイムで把握することがメインの目的。何がどれくらいの個数売れて、売上が何円なのかの把握。
最終的な売上の計上には使わないが(入力し忘れもあるだろうし)、その日の売れ行きの目安になる。
ついでに、どうせだったら似たような状況で使い回せるものをと、未受け渡しの注文の一覧や、会計時に合計金額を表示する機能などをつけたが、案の定その機能はあまり使わず。

その他個人的な目標

実は似たようなものを同年春の学祭でもAndroid・iOSネイティブアプリとして作成(当時Webの知識は皆無)。ただ、そのときはあまり使い物にならず。というのも…

  • レジ機能がメインだったものの模擬店程度なら暗算で事足りる
  • 一応データは溜めるもののローカルオンリーで皆で共有できない

そのリベンジも兼ねてメイン機能を売上把握に変えて作ろうとした。ただ、またもiOS・Androidそれぞれに作るのがとっっっても面倒くさかったので、Webの勉強のためにということでWebで作ることに。
HTMLタグくらいしか知らなかったところから数ヶ月で動的なものを作るの、背伸び感めっちゃあったけど気にしないことにした。

主に使ったもの

  • Firebase
    • Firestore
    • Firebase Hosting
    • Firebase functions
  • Webpack
  • React
  • Basic認証

サーバー持ってないし、そもそもそこらへんの知識もほぼないので、Firebaseの力を借りることに。Firestoreのデータはスマホからも弄れるし。
functionsを使っているのは、クラスの人の認証に簡易的にBasic認証を使ったため。
Webpackは色んなライブラリをFirebaseでも使えるようにするために使用。
内部の状態変化を画面と連動させるのにReactを使用。Angularとかもあるらしいけど最初に知ったのがReactだった。SPAにはしていない。

できたもの

GitHub
デモページ (オレンジの方(ICE AGE MAN)に ユーザー名:demo パス:demopass で入れます)
(悪質なサイトだと警告が出る可能性アリ。なにか脆弱性を含んでいるのかなあ…?)

見た目

デザインセンスはないのでご了承を。。

店舗トップ

店舗トップ(iPhone 6_7_8 Plus).png
今日と今まででそれぞれどれくらい売れたのかを表示。その下にメニューを配置。
メニューは上においても良かったかもだけど、メイン機能が売上把握なのでこの配置にした。

会計担当用ページ

注文画面(iPhone 6_7_8 Plus).png
何故かここだけ背景白だが特に意味はない。
ページ読み込み時にメニューをFirestoreから取得。注文を入力して注文確定したらそれがFirestoreへ送られる。「キャンセル」ボタンは「クリア」にしたほうが直感的だったかも。

注文一覧

注文一覧(iPhone 6_7_8 Plus).png
受け渡しが完了していない注文の一覧が表示される場所。実際の運用では使われず、ただただ完了ボタンを連打してストレス発散(?)する場所となった。

こだわりというか諸々

柔軟性

メニューのデータをハードコードせずFirestoreに預けることで、いつでもメニュー変更や値段変更に対応できるように。Firestoreにアクセスして手動で変えないといけないものの、手元のスマホでできるので、何度もあった値下げなどには割と簡単に対応できた。
また、データさえ作ってhtmlをコピペしてちょこっと改変すれば、簡単に他店舗にも流用できる。足りない機能さえなければ。

前売り券・セット割機能

前売り券はデータさえ変えれば単なる割引券にもできる(むしろ内部的には値段付きの割引券みたいな感じで処理している)。前売り券の値段のデータは一応設定してあるものの、払い戻し機能を結局つけなかったので何にも使われていない。
セット割は学祭の途中で急遽付け加えた機能。3個で500円とかそんな感じのやつ。いくつかセットがあっても最も安い値段を表示する…はず。

認証

店舗の人(クラスメイト)だという認証をどうしようか迷った。
できればFirebase Authenticationで完結させたかった。(Firestoreのアクセスルール設定でFirebase Authは使う。)
FirebaseAuthでは、会員登録ページとかは楽に用意できるらしい。
でも各々に登録してもらってそれを承認するのは手間がかかるし避けたい(誰か把握するのも大変だし)ので、楽そうなBasic認証にすることに(セキュリティもそんなに必要じゃないし)。Basic認証を通った後、Firestoreのルール用には匿名ログインで対応した。
もう少しセキュリティ必要なら手間をかける必要がありそう。
FirebaseAuthにメルアド-パスワードじゃなくてユーザー名-パスワード方式があればそれを使ったのだけれど。

firebase.initializeApp() まわり

このメソッドを複数回呼び出すと怒られる。けどSPAじゃないしどのページから入ってくるかわからないので、トップページに限らずどのページでも初期化の態勢は整えないといけない。
いちいち初期化済みかチェックしたあと初期化する処理を書くくらいならまとめてしまえということで、fbinitAnd(func)というメソッドを定義した。既に呼び出されていたらそのままfunc()を実行し、それ以外はinitializeしてからfunc()を実行。
非同期処理が関わってくるので少しめんどくさかった。もう少しスマートに書けないものかなあとも思うがこれ以上はわからない。勉強不足。

fbinit.js
import * as firebase from "firebase/app";

function fbinitAnd(func){
    if (!firebase.apps.length) {    // まだ初期化されてない
        fetch("/__/firebase/init.json").then(async response => {
            firebase.initializeApp(await response.json());
            func();
        }).catch( error => {
            throw error;
        });
    } else {    //初期化済
        func();
    }
}
export default fbinitAnd;

あとはtry-catchでfbinitAndの呼び出しを囲めば例外処理も上手くいくはず…はず…
今回例外処理には苦戦したので自信がない。

課題

Reactの描画更新処理

これを作った後、「React に優しい僕でありたい」の記事を読んでレンダリングを可視化してみたら、やっぱり無駄にレンダリングしていた。
レンダリングするかしないかを勝手に判断してくれてると勘違いしていたけどそうではないらしい。今回はそんなに重い処理をしているわけではないからいいけど、Reactとか使うとき留意しておきたいポイントだなと思う。

コードのコメントの量

できるだけわかりやすいように書くことを意識しているも、少し時間が経った今見ると、コメントはあまり書いていない。それでも見ればだいたいなんとなくわかる気もするけど、記憶が完全に抜けきってから見て理解できるかは怪しい。
セット割関連の処理は突貫工事だったのでそこらへんは別としても、もう少し意識していきたい。

Git関連

Gitflowを試しに使ってみたけれど、どの段階で新たにfeatureブランチ切るかとか、Gitそのものも含め慣れていかないとなあと。途中、面倒になってdevelopブランチで作業していたり…。

整理券番号機能

注文番号を自動で割り振る機能はあった。が、紙の整理券を使いまわしたりしていると、そっちに番号を合わせないといけないので注文時に整理券番号を入力する機能をつけておけばよかった。
あとそもそも注文番号が会計画面に表示されていないので使い物にならないし。
ここらへんをちゃんと整備さえしていれば、サークルの方の模擬店にもきちんと流用できたのになあと後悔。おかげで注文一覧画面が日の目を見ることはなかった。

その他感想

作ったものを実際に使って機能しているのを見るのは面白い。
ただ、どんどん増えていく売上を楽しむための可視化のつもりだったのに、肝心の売れ行きが不振だったのは残念。
あと、新しい分野を学ぶときはじっくり体系的に学んでいきたい。Web系は特に変化が速いし、そうも言ってられない気もするけど。
今更だけどこれじゃあブログでは…?Qiitaに投稿して大丈夫なのか?

参考にさせていただいたもの

Firebase チュートリアル FriendlyEats-React
最初見様見真似で始めるとき、ソースをめっちゃ参考にしていた。

Babelとwebpackを使ってES6でReactを動かすまでのチュートリアル
初期の参考第2弾

Webpackで複数のHTMLを出力する。
求めているのはSPAじゃないのでどうするかってときに参考になった。chunkの存在とか。

webpackのcss-loaderでCSS Modulesをやる
ReactでCSSを扱うときにどうするかってときこれを参考に。

[フロントエンド] React.jsのJSXで条件分岐を表現する方法(5つ)
レンダリング時に条件分岐がほしいとき。最初書き方に混乱しては見ていたのを覚えている。

「FirebaseでBasic認証をかけてサイトを作る(Hostingする)」
Basic認証簡単そうじゃんと思ったらFirebaseでやるにはそのままじゃだめだった。この記事を参考に導入。

「React に優しい僕でありたい」
これは本文中に出した記事。優しくありたい。

他にも沢山参考にしたけど、作るために漁っていたのがだいぶ前で、どれを参考にしたかいまいち覚えていない…。覚えている限りで。

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

<input type="file">をControlled的に使う

<input type="file">はReactから直接Controlledにはできませんが、用途によってはそれに近い使い方ができます。

フォーム要素の役割

以前にも書きましたが、HTMLのフォーム要素にはいくつかの役割があります。

  1. ユーザーからの値の入力
  2. 設定した状態の保存(Controlledにする場合は無効化される)
  3. 状態の表示
  4. フォーム送信時に値を伝える

このうち、「フォーム送信時に値を伝える」機能は、フォーム送信に頼らずAjaxを投げるような場面では不要となります1

<input type="file">の場合

そして、ファイル入力の場合は「状態の表示」も不要となることが多いです。デフォルトの<input type="file">のデザインは、ボタンとファイル名だけのそっけないものでカスタマイズも効かず、「デフォルトの要素を完全に殺す or 透明化させるなどして、自前で描画する」ということもよく行われるような代物です。

そして、ふつうのtype="text"のような場合であれば、すでに入力された値から再編集させるためにも元の値の表示は必要ですが、type="file"では「すでにセットされたファイルから編集する」ような機能は通常存在せず、ファイルがあろうがなかろうが既存のファイルを選択してアップロードさせるだけですので、この意味合いでももとの値をセットさせる意味は薄いです。

ということで、<input type="file">の場合は「新規登録の機能さえあればそれでいい」という場面もじゅうぶん考えられます。

新規登録専用のものを作ってみる

新規登録以外の機能性が不要な場合、onChangeのタイミングでフォームをリセットすれば可能そうですが、1つ注意点があります。

function FileAppendInput({onChange, ...rest}) {
  // リセット用のカウンター
  const [counter, setCounter] = React.useState(0);
  const handleChange = React.useCallback((e) => {
    onChange(e);
    // キーを変えて表示をリセット
    setCounter(current => current + 1);
  }, []);

  return <input {...rest} type="file" key={counter} onChange={handleChange} />

}

バグか仕様かは不明なのですが、value = ''でリセットすると、Chromeで「onChangeのタイミングで回収したFileListの中身が書き換わる」という現象が発生してしまったので、それを避けるためにkeyの書き換えでフォームをリセットしています。

filesを書き戻したい場合

フォーム要素へのファイルの再セットが必要な用途の場合、useEffectでファイルの変化を監視して、変わったときにref経由でfilesへセットすれば問題なく実行できそうですが、ここにもまた問題点があります。filesへのセットに必要なFileListをどう用意するかです。

前の投稿にまとめましたが、FileからFileListを作成することが、iOSなど一部の環境では実現不可能なのです。このような環境で<input type="file">への書き戻しが必要な場合、最初にセットされたFileListそのものを引き回すより他に手段がなくなってしまいます。


  1. ファイルについても、FormDataFileを追加して送信するという形で対応可能です。 

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

【React】CSSフレームワーク (ライブラリ)

概要

フロントエンドエンジニアとして見た目にこだわりたいですよね!
気になったCSSフレームワークや有名どころの物を一覧にしたので、ぜひ使ってみてください。

Semantic UI React

スクリーンショット 2020-02-22 16.53.27.png

MATERIAL-UI

スクリーンショット 2020-02-22 16.54.11.png

Ant Design

スクリーンショット 2020-02-22 16.54.57.png

Blueprint

スクリーンショット 2020-02-22 16.55.45.png

React Suite

スクリーンショット 2020-02-22 16.56.47.png

Sancho UI

スクリーンショット 2020-02-22 16.57.11.png

Polaris

スクリーンショット 2020-02-22 16.58.45.png

KendoReact (有料)

スクリーンショット 2020-02-22 16.59.52.png

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

気になっているReact (JavaScript) のライブラリ

概要

ライブラリの調査を普段やっていて、気になったものや、今話題になっているライブラリを一覧にしました。
気になっているレベルのライブラリがほとんどなので、もし使用したことのある方がいればぜひコメント欄で使用感を教えてください!

フォーム

React Hook Form

スクリーンショット 2020-02-22 15.53.02.png

スター数

  • 7,155 (2020/02/22)

特徴・気になった理由

使ってみた感想

useFormsを使用することで、コンポーネントに記述するフォームのState管理の処理が3分の1ほどに減らすことができたと思います。
用意されているPropsがFormik,ReduxFormと変わらないため、ほぼ使用感で高パフォーマンスのフォームを作成することができると思いました。

アニメーション

Popmotion

スクリーンショット 2020-02-22 16.13.39.png

  • 他にもUtilに特化したライブラリなどがありますが、今回はReactのために用意されたFlamer Motionを載せておきます!

Framer Motion

スクリーンショット 2020-02-22 16.16.07.png

スター数

  • 4,8k (2020/02/22)

特徴・気になった理由

  • hooksベース
  • サンプルが豊富
  • ドキュメントの見た目がかわいいw

React Simple Animate

スクリーンショット 2020-02-22 16.36.29.png

スター数

  • 1,270 (2020/02/22)

特徴・気になった理由

  • アニメーションを使用したい時に、記述が少なくすぐに実装できるため

react-animated-css

スクリーンショット 2020-02-22 16.40.39.png

スター数

  • 8,802 (2020/02/22)

特徴・気になった理由

  • Animate.cssのReact用ライブラリ

使ってみた感想

めちゃめちゃ簡単にアニメーションできる!!!

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

我々は結局、React Hooksをどう扱えば良いのか

React Hooksの目新しさも落ち着いてきて、様々な現場で当然のように見るようになってきました。
私がここ1年使ってきてHooksについて思うことを、まとめていきます。

React Hooksによって何が変わったか

React Hooksについて、公式には以下のようにあります。

フック (hook) は React 16.8 で追加された新機能です。state などの React の機能を、クラスを書かずに使えるようになります。

これだけ理解すると、

Class ComponentをFunctional Componentに書き換えられるだけでしょ?
ちょっとモダンな書き方になるだけで、本質は何も変わってないんでしょ?

と思ってしまいがちです。

Toggle.tsx
class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOpen: true };
  }

  handleClick() {
    this.setState(state => ({ isOpen: !state.isOpen }));
  }

  render() {
    return (
      <button onClick={ this.handleClick }>
        { this.state.isOpen ? 'ON' : 'OFF' }
      </button>
    );
  }
}

Toggle.tsx
const Toggle = () => {
    const [isOpen, setIsOpen] = useState<boolean>(true);
    const handleClick = () => setIsOpen(!isOpen); 

    return (
      <button onClick={ handleClick }>
        {isOpen ? 'ON' : 'OFF'}
      </button>
    )
}

パッと見すっきりしてるし、なんとなく良い書き方っぽい。
だからただHooks使えばなんとなくモダンな感じがする。

しかしHooksがそんなに浅いものだったら、ここまで話題にはなりません。
React Hooksの本質はそこではなく、Custom Hooksにあるのです。

Custom Hooks

Custom Hooksとは、簡単に言えば「Hooksを使用した関数」です。
それだけだと物凄いシンプルな話なんですが、見た目以上に強力な概念です。

useToggle.ts
export const useToggle = (initialValue: false) => {
  const [isOpen, setIsOpen] = useState<boolean>(initialValue);

  return {
    isOpen,
    open: useCallback(() => setIsOpen(true), []),
    close: useCallback(() => setIsOpen(false), []),
    toggle: useCallback(() => setIsOpen(!isOpen), [isOpen])
  };
};

状態とそれに対する操作のスコープを制限することができる

Class内には、そのコンポーネントの持つ全ての状態が列挙されます。
バケツリレー的な書き方が推奨されることが多い関係上、そのRootComponentにはあらゆる状態が集まりがちでした。

class UserShowContainer extends Component {
  constructor(props: Props) {
    super(props);
    this.state = {
      isLoading: true,
      isError: false,
      isLogouting: false,
      user: null,
      item: null,
      isItemDialogOpen: false,
      isDeleteDialogOpen: false,
      ...
    };
    ...
  }

これらの状態には、お互いに関係性のないもの多くあります。
しかしこれらは「そのComponentに属する」だけで全て同じスコープとして扱われます。
そのため可読性が落ち、バグの温床になることも多々ありました。

たとえばAPIのエラー判定をisErrorで行っていたとします。

  async fetch() {
    try {
      this.setState({ isLoading: true });
      await fetchUser();
      this.setState({ isLoading: false, isError: false });
    } catch (e) {
      this.setState({ isLoading: false, isError: true });
    }
  }

追加の実装でこんなコードが書き足されたとします。

  async fetchItem() {
    try {
      this.setState({ isLoading: true });
      await fetchItem();
      this.setState({ isLoading: false, isError: false });
    } catch (e) {
      this.setState({ isLoading: false, isError: true });
    }
  }

お互いに同じisLoadingisErrorを操作してしようとしまって、たとえばエラーのはずが処理の順番でエラー扱いではなくなってしまったという不具合につながります。
もちろんこれだけ見ると「こんなコード書くほうが悪い」って話ですが、Componentが肥大化していくとこういう問題を防ぐのは難しくなります。

Redux等も書き込み自由なグローバル変数のように扱われがちで、こうした「状態に対する意図しない操作」を防ぐことが困難でした。

一方Custom Hooksを使えば、状態とそれに対する操作のスコープを制限することができます。

const useFetchUser = () => {
    const [isLoading, setIsLoading] = useState<boolean>(false);
    const [isError, setIsError] = useState<boolean>(false);
    const [user, setUser] = setState<User | null>(null);

    const fetch = async () => {
        try {
            setIsLoading(true);
            setUser(await fetchUser());
            setIsError(false);
        } catch {
            setIsError(true);
        } finally {
            setIsLoading(false);
        }
    });

    useEffect(() => { fetch() }, []);

    return {
        user,
        isLoading,
        isError
    }
}

この書き方において、isLoadinguserといった状態が外部から操作される危険性は全くありません!

ただuser, isLoading, isErrorが正しく返却されることのみ担保されれば良いので、テスタビリティも向上します。
また同じ挙動が担保されるのであれば、内部の実装の変更は自由です。

後からReduxで書き換えることもできますし、事情があって状態の持ち方を変える場合も柔軟に対処可能です。
たとえば次の例ではisOpenbooleanからnumberに変えていますが、影響範囲はCustom Hooksのスコープ内のみに閉じ込められています。

export const useToggleBool = () => {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  return {
    isOpen,
    open: () => setIsOpen(true),
    close: () => setIsOpen(false)
  };
};

export const useToggleNumber = () => {
  const [isOpen, setIsOpen] = useState<number>(0);

  return {
    isOpen: isOpen === 1,
    open: () => setIsOpen(1),
    close: () => setIsOpen(0)
  };
};

またStateに対して許可する操作のみを関数化できるため、意図しない値が入ることを防ぐことが可能です。
例えば上記のuseToggleNumberにおいて、isOpen01以外の値が入ることを想定しなくても良いのです。

状態とそれに対する操作を使い回すことができる

Custom Hooksを定義することで、状態と、その状態に対する操作を使いまわすことができます。

HomeComponent.tsx
const HomeComponent = () => {
    const userDialog = useToggle();
    const itemDialog = useToggle();

    return (
        <>
            <UserCard onClick={userDialog.open} />
            <Button onClick={itemDialog.toggle} />
            {userDetailDialog.isOpen && <UserDialog onClose={userDialog.close} />}
            {itemDialog.isOpen && ItemDialog />}
        </>
    )
}

もちろんClass Componentにも、高階コンポーネントなどロジックの再利用を行う手段はありました。
が、どうしても複雑になりがちでした。

気軽に再利用ができるようになったことで、たとえば以下のような「HeadlessなUIライブラリ」も生まれきています。

ex) https://github.com/tannerlinsley/react-table/blob/master/docs/concepts.md

今までは再利用性の高いComponentを持つことが生産性向上につながり、企業は独自のComponent Libraryを作るなどしてこれを図ってきました。
今後はいかに使い回しの効くHooksを持っているかも、重要になってくるでしょう。

状態をComponentから切り離すことができる

Class Componentでは、「UIを作り上げるためのロジック」や「状態を変化させるためのロジック」がクラス内に混合しがちでした。
そのためReduxなどのFlux Architectureが普及し、ComponentではUIの生成のみに注力する書き方が主流となりました。

しかしHooksにより、状態をComponentから切り離すことが可能となりました。
そのため状態の分離のために、Reduxに頼る必要がなくなったのです。

特にReduxはSingletonな1つのStateをProjectで共有するため、一部の状態のみを独立して扱うことが困難でした。
他の状態からも切り離し可能になったことで、フロントエンド設計の柔軟性は大幅に向上しました。

React Hooksで解き放たれた状態なにで縛るか

さて、React Hooksにより「状態」はComponentから時なはたれ、自由となりました。
しかしルールのない自由は、ただの混沌です。

Custom Hooksも、どの粒度で、どういう単位で設計すれば良いのか、利用者に委ねられています。
Custom HooksからCustom Hooksを呼び出すこともできるため、その設計の自由度は計り知れません。

では、どのようなルールでCustom Hooksを設計すれば良いのでしょうか?

クラス定義っぽい?

まずCustom Hooksを書いてて思ったのが、クラス定義っぽい!ってことです。

const toggleA = useToggle(); // const toggleA = new Toggle();っぽい
const toggleB = useToggle(); // const toggleB = new Toggle();っぽい
if (toggleA.isOpen && toggleB.isOpen) {
    toggleA.close();
}

getterとsetterを内部に隠蔽し、利用者に必要な操作のみを提供するのはカプセル化っぽい!
「じゃあ試しにhooksをドメインモデル単位にして、モデルに対して可能な操作をそこで許可したらどうだろう?」と、試してみました。

const useUser = (userId: number) => {
    ...
    return {
        destroy: () => ...,
        fetch: () => ...,
        isLoading
    };
}

確かに使ってる分にはそれっぽい。
user.destroyだのuser.isLoadingだの、使ってる分にもわかりやすい。
雑に便利に使える感じはRailsっぽい。

ただ1つのhooksが担保する責務が広すぎました!

クラス定義はただのデータの型なのに対し、HooksはComponentに紐づく状態や、場合によってはAPIアクセエスも含みます。
気軽に使いまわせるものではなく、どうしても利用側の状況に合わせちょっとした差異が生じてきます。
結局のところそうしたモデルに対する操作の定義はClassに任せ、HooksはそのClassを扱う程度に止めるのが良いと感じました。

クラス定義と同一視するには、さすがにHooksの持つ意味合いは広すぎました。

Component始点でHooksを定義する

この反省を踏まえて考え直すに、hooksはあくまでComponentの持つ状態の延長線上です。
hooks設計の視点としてComponentが始点となるのは避けられないと感じました。

ということで、まずはComponentの持つ状態を整理します。

このコンポーネントの持つ状態は何か?

- APIの読み込み状態
- APIの取得の結果エラーだったか
- 検索フォームに撃ち込まれた文字
- ダイアログの開閉状態

その上で、互いに関係性のある状態をグルーピングし、その最小単位をCustom Hooksにしていきます。

const useFetchUser = () => {
   const [user, setUser] = useState<User>();
   const [isLoading, setIsLoading] = useState<boolean>(false);
   const [isError, setIsError] = useState<boolean>(false);
   ...
}

export const useInput = () => {
  const [value, setValue] = useState<string>('');

  return {
    value,
    onChange: (e: React.FormEvent<HTMLInputElement>) =>
      setValue(e.currentTarget.value)
  };
};

用途からではなく、本質的な意味単位で状態設計を行う

このとき利用者側の基準で状態設計すると、1つの状態の持つ意味合いが広くなりがちです。
たとえば初期化中はローディングを出したいと思ってisLoadingという状態設計すると下記のようになります。

   const [isLoading, setIsLoading] = useState<boolean>(false);

   setIsLoading(true);
   await Promise.all([fetchUser(), fetchCompany(), fetchItems()]);
   setIsLoading(false);
}

しかしこれだと1つのCustom Hooksの持つ意味が広くなりすぎ使い回しが効きづらくなります。
また後に「ユーザがロード中の時だけユーザのアイコンを変えたい!」となったとき、変更が大変です。

fetchUserisLoadingfetchCompanyisLoadingは本質的に互いに関係がない値です。
より本質的な意味単位で状態設計し、そこから各UIの事情に合わせ値を計算したほうが良いでしょう。

const fetchUser = useFetchUser();
const fetchCompany = useFetchCompany();
const fetchItems = useFetchItems();

const isLoading = fetchUser.isLoading || fetchCompany.isLoading || fetchItems.isLoading;

Custom Hooks内で共通する処理は、さらに抽象化する

各Custom Hooksで共通するような処理は、より汎用的なCustom Hooksで抽象化していきます。
Componentの事情に近いCustom Hooksから、より抽象化の高いCustom Hooksを呼び出すような構造となります。

export const useFetchUser = () => {
    const [user, setUser] = useEffect<User | null>(null);
    const fetch = useAsync(async () => setUser(await userApi.fetch()));
    ....
}

export const useFetchCompany = () => {
    const [company, setCompany] = useEffect<Company | null>(null);
    const fetch = useAsync(async () => setCompany(await companyApi.fetch()));
    ...
}

export const useAsync = (callback) => {
    const [isLoading, setIsLoading] = useEffect<boolean>(false);
    const [isError, setIsError] = useEffect<boolean>(false);

    ...
} 

課題

ただこの考え方でも、まだしっくりこない点があります。
たとえば編集画面でuseFetchUseruserを編集したいときです。

useFetchUserの中にsetUserNameなどを生やすと、このCustom Hooksの責務がどんどん広くなっていきます。

またsetUserを返すようにすると、どんどんガバガバになっていき、Custom Hooksのメリットは薄れてしまいます。

パラメータや戻り値でなんとかしていこうとすると、運用が続くにあたりCustom Hooksに渡すパラメータは増え、戻り値もどんどん増えていきます。

この辺りは適度にバランス感を取りつつ、継続的なリファクタリングを行う必要があるでしょう。

おわりに

というわけで確信めいた結論は出ていないものの、現時点での考えを記事にまとめました。

実際のHooksの例としては、下記Repsoitoryが非常に参考になるので、最後に紹介だけしておきます!
https://github.com/streamich/react-use

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

create-react-appで React + Typescript な環境を構築する

はじめに

facebook社公式のReactプロジェクト生成ツールである create-react-app
2018/10/29リリースの v2.1.0 でTypeScriptをサポートするようになりました。

とても簡単に React + Typescript の開発環境を構築できるようになっているので、その方法をまとめています。
よりよい方法やバグ等ございましたら、アドバイスいただけると光栄です。

尚、執筆時のバージョンは以下のようになっております。
最新の方法について知りたい方は 公式ドキュメント をご覧ください。

$ node -v
v13.9.0

$ yarn -v
1.22.0

$ npx create-react-app --version
3.3.0

環境構築

0. 事前準備

各種インストールの確認をします。

0-1. Node.js

下記コマンドで、Node.js がインストールされているか確認します。

$ node -v
v13.9.0

まだインストールされていない場合は、公式ドキュメント を参照の上、インストールしてください。

※ MacOSの方は以下の方法もおすすめです。
MacにNode.jsをインストール(anyenv + nodenv編) - Qiita

0-2. yarn

パッケージマネジャーは yarn を使用するため、
下記コマンドで、yarn がインストールされているか確認します。

$ yarn -v
1.22.0

まだインストールされていない場合は、公式ドキュメント を参照の上、インストールしてください。

1. 新規プロジェクトを作成

実際に create-react-app でプロジェクトを作成します。

1-1. create-react-app

以下のコマンドを叩くだけでプロジェクトを作成できます。

$ npx create-react-app {プロジェクト名} --typescript

{プロジェクト名}には、自分のつけたいプロジェクト名を設定してください。
例えば、react-ts-app というプロジェクトを作成したい場合は、以下のコマンドを入力します。

例)
$ npx create-react-app react-ts-app --typescript

Happy hacking! という文字が表示されれば完了です。

1-2. プロジェクトを起動

作成したプロジェクトのディレクトリに移動し、起動します。

$ cd {プロジェクト名}
$ yarn start

先ほどの例に挙げた react-ts-app というプロジェクトを作成した場合は、以下のコマンドを入力します。

例)
$ cd react-ts-app
$ yarn start

http://localhost:3000 がブラウザで開き、以下のような画面が表示されれば完了です。
image.png

2. 開発環境設定

開発するに当たり便利な各種設定をします。

2-1. コミット時にコードを自動整形する

コミット時にコードを自動整形したいので、下記3つをインストールします。

  • husky
  • lint-staged
  • prettier

以下のコマンドを入力すれば、インストールができます。
開発時のみに必要なライブラリなので、-D を付けてすべて devDependencies としてインストールします。

$ yarn add -D husky lint-staged prettier

※ dependencies と devDependencies の違いについては、下記を参照ください。
package.jsonにおけるdependenciesとdevDependenciesの違い(超シンプルに) - Qiita

インストールが完了したら、package.json を開いて必要なものを足していきます。

2-1-1. lint-staged

拡張子が ts または tsx の場合に、prettier を走らせてから git add するように、package.json の最後あたりに下記を追記します。

pakege.json
"lint-staged": {
  "*.{ts,tsx}": [
    "prettier --write",
    "git add"
  ]
}

2-1-2. husky

precommit 時に上記の lint-staged が実行されるように、package.json の最後あたりに下記を追記します。

pakege.json
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
}

2-1-3. prettier

コードフォーマット関連の設定を追加します。
こちらも、package.json の最後あたりに下記を追記します。

pakege.json
"prettier": {
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all"
}

ここでは以下のような設定をしています。

  • semi : 末尾にセミコロンをつけるかどうか
  • singleQuote : シングルクォートに変換するかどうか
  • tabWidth : インデントのスペースの数
  • trailingComma : 複数行の場合に末尾にカンマをつけるか

※ その他のオプションについては、prettierの 公式ドキュメント で確認できます。

2-2. 型チェックによる安全性を最大化する

せっかく TypeScript を使用しているので、

  • 型チェックによる安全性を最大化したい。
  • あわよくば、不要なコードも怒ってくれるとうれしいなー

ということで、tsconfig.json を下記のように変更します。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "strictNullChecks": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react"
  },
  "include": [
    "src"
  ]
}

ここでは、下記のオプションを追加しました。

  • strictNullChecks : nullやundefinedが「一人前の型」になる。
  • noImplicitAny : 暗黙のany型を禁止する。
  • noImplicitThis : thisに型を指定していない場合にエラーを発生させる。
  • noImplicitReturns : 関数内のすべての経路で、返り値の型があっているかをチェックする。
  • noUnusedLocals : 宣言して、一度も使われないローカル変数があると、コンパイルエラーになる。
  • noUnusedParameters : 一度も使われないパラメータがあると、コンパイルエラーになる。

※ その他のオプションについては、TypeScriptの 公式ドキュメント で確認できます。

2-3. import を絶対パスにする

相対パスで import するのは色々と面倒なので、絶対パスで指定できるようにします。

以下のように tsconfig.json ファイルの compilerOptionsbaseUrl を追加します。

tsconfig.json
{
  "compilerOptions": {
    ...,
    "baseUrl": "src"
  }
}

これで、絶対パス使えるようになりました。
例えば、src/components/Button.tsx にあるモジュールをインポートする場合、以下のようにインポートできます。

例)
import Button from 'components/Button'

※ 詳しい方法は 公式ドキュメント で確認できます。

終わりに

こちらの方法で作成したプロジェクトのサンプルをGitHubに公開していますので、よろしければ参照ください。
https://github.com/genki-sano/react-ts-app

参考

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

【react-native-paper】BottomNavigationを実装してみた

まずはじめに

react-native-paperが提供しているBottomNavigationはreact-navigationが提供しているBottomtabNavigationとかなり似ているのですが、ただアニメーションがついていたりちょっとリッチな感じになっています。
今回目指す実装はこういった実装です↓
Feb-22-2020 13-25-26.gif

react-native-paperの導入

過去導入手順をまとめた記事があるので参考にしてください
導入手順

公式ドキュメント通りBottomNavigationを実装してみる

ただ公式ドキュメント通りやるとMusicタブのアイコンがうんたらかんたらと言われるので、MUSICを消します。

App.js
import React from 'react';
import { Provider as PaperProvider } from 'react-native-paper';
import AppNavigator from './navigation/AppNavigator'

export default function App() {
  return (
    <PaperProvider>
      <AppNavigator />
    </PaperProvider>
  );
}
/navigation/AppNavigator.js
import * as React from 'react';
import { BottomNavigation, Text } from 'react-native-paper';

const AlbumsRoute = () => <Text>Albums</Text>;

const RecentsRoute = () => <Text>Recents</Text>;

export default class AppNavigator extends React.Component {
    state = {
        index: 0,
        routes: [
            { key: 'albums', title: 'Albums', icon: 'album' },
            { key: 'recents', title: 'Recents', icon: 'history' },
        ],
    };

    _handleIndexChange = index => this.setState({ index });

    _renderScene = BottomNavigation.SceneMap({
        albums: AlbumsRoute,
        recents: RecentsRoute,
    });

    render() {
        return (
            <BottomNavigation
                navigationState={this.state}
                onIndexChange={this._handleIndexChange}
                renderScene={this._renderScene}
            />
        );
    }
}

Feb-22-2020 13-41-12.gif

一旦タブでの動きは完成しました。

タブに色を指定してみる

タブに色を付与する場合は、state.routesの各画面にcolorタグを付与してあげます。

    state = {
        index: 0,
        routes: [
            { key: 'albums', title: 'Albums', icon: 'album', color: '#000000' },
            { key: 'recents', title: 'Recents', icon: 'history', color: '#009688' },
        ],
    };

確認してみる

あれ変わってない!!なんで変わってないんだろ・・・
と思ったら同じこと思っている人がいました。

callStack

どうやらshifting={true}が必要みたいです。追加していきます。

    render() {
        return (
            <BottomNavigation
                navigationState={this.state}
                onIndexChange={this._handleIndexChange}
                renderScene={this._renderScene}
                shifting={true}
            />
        );
    }

ついでにスタイル等も整えてみます。
修正したAppNavigator.jsは以下です。

/navigation/AppNavigator.js
import * as React from 'react';
import { View, StyleSheet } from 'react-native'
import { BottomNavigation, Text } from 'react-native-paper';

const AlbumsRoute = () =>
    <View style={styles.textStyle}>
        <Text >Albums</Text>
    </View>;

const RecentsRoute = () =>
    <View style={styles.textStyle}>
        <Text >Recents</Text>
    </View>;
export default class AppNavigator extends React.Component {
    state = {
        index: 0,
        routes: [
            { key: 'albums', title: 'Albums', icon: 'album', color: '#000000' },
            { key: 'recents', title: 'Recents', icon: 'history', color: '#009688' },
        ],
    };

    _handleIndexChange = index => this.setState({ index });

    _renderScene = BottomNavigation.SceneMap({
        albums: AlbumsRoute,
        recents: RecentsRoute,
    });

    render() {
        return (
            <BottomNavigation
                navigationState={this.state}
                onIndexChange={this._handleIndexChange}
                renderScene={this._renderScene}
                labeled={true}
                shifting={true}
            />
        );
    }
}

const styles = StyleSheet.create({
    textStyle: {
        flex: 1,
        justifyContent: "center",
        alignItems: "center",
    },
})


Feb-22-2020 13-54-21.gif

最後に

stackcallの記事を見つけるまでに時間かかってしまったので、投稿しました。
個人的にこのアニメーションのタブは好きなのでこれから多様して何か作っていきます。
記事良かったら共有などお願いします!

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

webサービスを運営してみた(2020/2/22)

はじめに

アルバイトの勤怠管理を無料でできるサービスTimestampを個人で運営しています。
フルタイムで仕事をしつつ個人のサービス運営で売上を上げられるのかの実験だったり、技術向上が目的だったりします。
ここでは運営や開発に関する記録を残していきます。
2020年2月に始めたばかりでサービスとしてはまだまだな状態です!

ユーザー数

スクリーンショット 2020-02-22 3.41.50.png
googleアナリティクスのグラフです。
ユーザ数が先週より伸びてますね。嘘です0人になってますね?
アカウント登録者数は2人でアクティブなアカウントは0です。

雑記

コードは自動生成されたものなんです

バックエンドはNodeとExpress、フロントエンドはReactとMateriau-ui、データベースはMysqlを使用しているのですが、これらは自動生成されたコードを利用してます。コード自動作成サービスScaffoldHubで作成しました。ReactやVueなどの言語を選んだり、データベースのテーブル設計もできます。自動生成されたコードのデモを動かすところまでは無料で楽しかったので、触ってみてはどうでしょうか。コードをダウンロードしようとすると有料です。
Timestampのコードの90%ぐらいは自動生成されたコードですね。

開発予定

しばらくは機能開発に時間がかかりそうです。作業時間を確保できてるしモチベーションも保ててはいるのですが、
それでもなかなか進まなくてもどかしい。
そのあとはSEO対策、英語翻訳を外注、LPのデザインを外注しようかなと考えているのですが、
外注できるかは料金次第ですね。。

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

既存のJavaScript (Node.js)をTypeScriptにする

TL;DR;

  • 迷ったら最初からTypeScript入れておくが吉
  • 移行するならimplicit*系のオプションはOffにして優しいTypescriptに
  • tsc + nodemonで効率のいい開発

この記事の対象

  • 今あるJavaScriptで書かれたプロジェクトにTypeScriptを導入したい人
  • JavaScriptに少しずつTypeScriptを入れたい人
  • Node.jsで動かすことを想定しています。(フロントエンドではない)

1. まずTypeScriptを導入する

これは簡単。yarn add --dev typescriptだけ。

2. tsconfigを書く

これが面倒。include, excludeは各自頑張ってください。どこかのを参考にすればどうにかなります。(node_modulesとかをexcludeに入れるのを忘れずに。とっても重くなります)
compilerOptionsで悩んだので、必要そうなところを載せます

  • targetここ見て自分のNodeがどこまで対応しているかで決めるがいいと思います。Node 13を使っているのならes2019まで対応しているのでes2019でいいかと思います。

  • libは「es2018にはないけれど、これもあることにしてね」とtypescriptに伝える設定です。polyfillされるわけではないことに注意してください

  • implicit*は、導入前に死なないようにとりあえずfalseにしておくことをおすすめします

  • implicitAnyは個人的にずっとfalseでいいように思います。必要なところだけ形を書いて安全にしていけるところがJavaScriptをTypeScriptのいいとこどりができると思いますし、一番効率的かと思います。

tsconfig.json

{
  "compilerOptions": {
    "moduleResolution": "node",
    "target": "es2019",
    "module": "commonjs",

    // ReactのSSRをしている場合はこの辺の設定が必要
    "jsx": "react",
    "lib": [
      "dom",
      "es2019.array" // targetがes2017だけどArray.flatとか使いたいよーと言う場合に追加してください。他にもいろいろあります。
    ],

    // 段階的に移行するならtrueにします。tsファイルからjsを読み込めるようになります。逆は無理
    "allowJs": true,

    // jsファイルの形チェックもしたければtrueにしましょう。全部のエラーに対応しなければいけなくなるのでfalseがおすすめ
    "checkJs": false,

    // d.tsファイルのtypeチェックを無効化します。(falseだとnode_modules内でエラー出まくりました)
    "skipLibCheck": true,

    // 後述するモジュールと組み合わせるとtsファイルの行数でスタックが見られるので楽です
    "sourceMap": true,

    // デコレーター使いたい場合はtrueで。Reactで使っているので必要でした。
    "experimentalDecorators": true,

    // TypeScript 3.4以上で使えます。watchしながらのトランスパイルが爆速になります。付けない理由はない。
    "incremental": true,

    // ここから先はlint的な要素です

    // JavaScriptから移行する場合、この辺はfalseにしておかないとかなり辛いです。特にanyは無理です。
    "noImplicitAny": false,
    "noImplicitReturns": false,
    "noImplicitThis": false,

    // 使っていない変数が定義されてたら怒られるオプション。これはes-lintとかを導入していればトランスパイル自体はエラーにしなくてもいいかもしれません。
    "noUnusedLocals": true,

    // ここも移行していく場合とりあえずfalseでいいと思います
    "strictNullChecks": false, // trueにすると例えば`number`にnullやundefinedを代入できなくします
    "strictFunctionTypes": false,
    "strictBindCallApply": false,
    "strictPropertyInitialization": false,
  },
}

3. 変更があったらサーバーを自動で再起動とかしたい

全部入りなts-nodeを使う方法もありますが、今回はtscコマンド + nodemonを使います
トランスパイル済みjsは./distに吐き出されるものとします。

nodemon.jsonを作ります。distをwatchして、変更があれば"exec"に書かれている内容を実行します。この場合server.jsの再起動です
"restartable"はその文字を打つとexecを強制的に再実行します。あんま使いませんがたまに便利です。

nodemon.json
{
  "restartable": "rs",
  "watch": [
    "./dist",
  ],
  "exec": "node ./dist/server.js"
}

まずtypescriptのトランスパイルを実行します
yarn tsc --watch -p tsconfig.json

次にnodemonを起動します
nodemon --config ./nodemon.json

これで、
ファイルを変更したらtypescriptがトランスパイル -> 吐き出されたファイルをnodemonが検知して再起動
となり効率の良い開発が可能になります。

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