20200922のReactに関する記事は7件です。

next.jsをGAEにデプロイ

概要

Google App Engine にnext.jsで実装したFrontendサービスをデプロイするための手順とポイントについてまとめます。
また、gcloud SDKはローカル環境で利用可能な状態であることを想定します。

package.json

最初にpackage.json内に記載のscriptをGCPにデプロイするために編集します。(これは必須ではありませんが開発効率向上のため設定)

package.json
{
    "scripts": {
        "dev": "next",
        "build": "rm -rf ./build && NODE_ENV=production next build",
        "start": "next start",
        "deploy": "npm run build && gcloud app deploy",
        "export": "next export"
    },
    "dependencies":{

       '''''''''''''略'''''''''''''''''''

    }
}

デプロイのために書き加えた部分はbuildとdeployです。

まずbuildの部分ですが、nextの場合buildすると通常は.nextディレクトリ下にビルドされますが、GAEはこれを認識できないのでbuildフォルダを作成してその中にビルドファイルを入れるようにします。
次にdeployではnom run deployを実行してそのまままGAEへのデプロイも行うようにしてあります。

また、たまにstartのスクリプトをnext start -p 8080としている記事を目にしますが、この記述はパフォーマンスの面で良くありません(スケーリング時、不必要に何度も実行されてしまう。)ので、次のapp.yamlの中のentrypointで記述するようにします。

app.yaml

app.yaml
service: front
runtime: nodejs12
entrypoint: next start -p $PORT
handlers:
- url: .*
  script: auto

今回はfrontendとbackendを分けているのでserviceにforntと記述しておくことでGAE内でのserviceとして独立させています。
entrypointの部分でnext start -p $PORTと記述することでアプリ起動時のコードを設定できます。
アプリの起動時に entrypoint コマンドを実行することで、デフォルトの起動動作をオーバーライドすることができ、今回の場合だと環境変数PORTにポートを開いてアプリをスタートすることができます。
entorypointの記述が誤っているとHTTP500エラーが出てしまう場合があるので、entrypointに正しく記述をすることで修正できます。

あとはpackage.jsonで設定したnpm run deployを実行してあげることで自動的にGAEにデプロイされます。

終わり

今回の例は自分の実行環境下での1例であり、あくまで参考程度にしていただけたらと思います。
間違っている部分等があればご指摘よろしくお願いします。

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

[React / amCharts4] ローソク足(candlestick chart)を描画

概要

amChartsで、Reactアプリケーション上にローソク足(candlestickチャート)を描画するまで。

なぜamCharts?

同一チャートエリア内に、candlestickチャートとscatterを描画したかったが、以下のライブラリではできないか、またはできなさそうと判断したため。
無料で使用できる有名どころのライブラリをいくつか調べたところ、amChartsであればできそうだったため、消去法で採用。

ChartJS, ApexChart, HighCharts 不採用の理由詳細
  • ChartJS
    candlestickを扱っているような動画としょぼいデモページはあるが、ドキュメントに記載がない?(見つからない)
    そして、この動画に掲載されているWEBサイトはどこに?
  • ApexChart
    実際に使ってみたが、問題があった

    • candlestickチャートのxaxisのtypeとして'category'を指定すると
      表示はできるものの、scatterのx軸上の位置を指定することができず、scatterの表示箇所が全て左端に寄ってしまった
    • candelstickチャートのxaxisのtypeとして'datetime'を指定すると
      scatterをx軸上の期待した位置に表示することができるが、'datetime'では、取引所が閉まっている期間(つまり、データが存在しない期間)のローソク足の位置に無駄な隙間が空いてしまうため、'datetime'は不適切
  • HighCharts
    実際に使ってみたわけではないが、調べた限りでは、scatterとcandlestickを同一チャート内に描画することができなさそう
    別々のオブジェクトに対して描画しているサンプルしか見つけることができなかった

※いずれも、2020/09/22時点の状況のため、状況が変わっている可能性があります

これに対してamChartsでは、candlestickscatterを同一のXYChartというオブジェクト上に描画できそうであった

他にも多少調べてみたが、料金がかかるものだったり、ドキュメントが不親切でどこまでできるかよくわからないものしか見つけることができなかった。
(GoogleChartは評判が良くなさそうだったので調べてすらいない。)
ちなみに、料金がかかるものは非常に良いものだったので、本当はそっちを使いたかった。

準備

$ npm install @amcharts/amcharts4

実装

1. まずは Getting Started で基本的なグラフから

公式ドキュメントのCreating_a_chartに書かれている通りにコーディングすれば、すぐに以下のようなグラフを確認できる。

image.png

右端の日付が見切れているが、ウィンドウサイズに対してレスポンシブな上に、表示範囲の拡大縮小時の動作がすばらしく美しい。

ChartJS と ApexChart は使用経験がありますが、そのどちらよりも相当洗練されています。
これはかなり期待できる(`・ω・´)

2. 次に candlestick チャートを描画

やり方はここに書いてあります。
が、掲載されているものから若干修正したので、一応修正後のソースコードも掲載。

ソースコード

ソースコードはここをクリックして確認
src/App.js
import React, { Component } from 'react';
import AmChartSample from './AmChartSample';

class App extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    return (
      <>
        <AmChartSample />
      </>
    );
  }
}

export default App;
src/AmChartSample.js
import React, { Component } from 'react';
import * as am4core from "@amcharts/amcharts4/core";
import * as am4charts from "@amcharts/amcharts4/charts";
import am4themes_animated from "@amcharts/amcharts4/themes/animated";

am4core.useTheme(am4themes_animated);


class AmChartSample extends Component {
  componentDidMount() {
    this.chart = prepareChart();
  }

  componentWillUnmount() {
    if (this.chart) {
      this.chart.dispose();
    }
  }

  render() {
    return (
      <div id="chartdiv" style={{ width: "100%", height: "500px" }}></div>
    );
  }
}

function prepareChart() {
  // ... chart code goes here ...
  let chart = am4core.create('chartdiv', am4charts.XYChart);
  chart.paddingRight = 20;
  chart.dateFormatter.inputDateFormat = 'yyyy-MM-dd';

  let dateAxis = chart.xAxes.push(new am4charts.DateAxis());
  dateAxis.renderer.grid.template.location = 0;
  dateAxis.renderer.minGridDistance = 60;
  // INFO: dateAxis.skipEmptyPeriods を true にしておくと、休日の位置に無駄な余白が表示されずに済む
  // dateAxis.skipEmptyPeriods = true;

  let valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
  valueAxis.tooltip.disabled = true;
  valueAxis.renderer.minWidth = 35;

  var series = chart.series.push(new am4charts.CandlestickSeries());
  series.dataFields.dateX = "date";
  series.dataFields.valueY = "close";
  series.dataFields.openValueY = "open";
  series.dataFields.lowValueY = "low";
  series.dataFields.highValueY = "high";
  // ${変数} を "" や '' で囲むとwarningメッセージが表示されるので`$`を削除した
  series.tooltipText = "Open: [bold]{openValueY.value}[/]\nLow: [bold]{lowValueY.value}[/]\nHigh: [bold]{highValueY.value}[/]\nClose: [bold]{valueY.value}[/]";

  chart.cursor = new am4charts.XYCursor();

  let scrollbarX = new am4charts.XYChartScrollbar();
  scrollbarX.series.push(series);
  chart.scrollbarX = scrollbarX;

  // amChartsのCodePenはこっちのスクロールバーを使っていたけれど、上のやつの方がきれいじゃない?お好みで
  // chart.scrollbarX = new am4core.Scrollbar();

  chart.data = candleData;

  return chart;  
}

const candleData = [{
  "date": "2018-08-01",
  "open": "136.65",
  "high": "136.96",
  "low": "134.15",
  "close": "136.49"
}, {
  "date": "2018-08-02",
  "open": "135.26",
  "high": "135.95",
  "low": "131.50",
  "close": "131.85"
}, {
  "date": "2018-08-05",
  "open": "132.90",
  "high": "135.27",
  "low": "128.30",
  "close": "135.25"
}, {
  "date": "2018-08-06",
  "open": "134.94",
  "high": "137.24",
  "low": "132.63",
  "close": "135.03"
}, {
  "date": "2018-08-07",
  "open": "136.76",
  "high": "136.86",
  "low": "132.00",
  "close": "134.01"
}, {
  "date": "2018-08-08",
  "open": "131.11",
  "high": "133.00",
  "low": "125.09",
  "close": "126.39"
}, {
  "date": "2018-08-09",
  "open": "123.12",
  "high": "127.75",
  "low": "120.30",
  "close": "125.00"
}, {
  "date": "2018-08-12",
  "open": "128.32",
  "high": "129.35",
  "low": "126.50",
  "close": "127.79"
}, {
  "date": "2018-08-13",
  "open": "128.29",
  "high": "128.30",
  "low": "123.71",
  "close": "124.03"
}, {
  "date": "2018-08-14",
  "open": "122.74",
  "high": "124.86",
  "low": "119.65",
  "close": "119.90"
}, {
  "date": "2018-08-15",
  "open": "117.01",
  "high": "118.50",
  "low": "111.62",
  "close": "117.05"
}, {
  "date": "2018-08-16",
  "open": "122.01",
  "high": "123.50",
  "low": "119.82",
  "close": "122.06"
}, {
  "date": "2018-08-19",
  "open": "123.96",
  "high": "124.50",
  "low": "120.50",
  "close": "122.22"
}, {
  "date": "2018-08-20",
  "open": "122.21",
  "high": "128.96",
  "low": "121.00",
  "close": "127.57"
}, {
  "date": "2018-08-21",
  "open": "131.22",
  "high": "132.75",
  "low": "130.33",
  "close": "132.51"
}, {
  "date": "2018-08-22",
  "open": "133.09",
  "high": "133.34",
  "low": "129.76",
  "close": "131.07"
}, {
  "date": "2018-08-23",
  "open": "130.53",
  "high": "135.37",
  "low": "129.81",
  "close": "135.30"
}, {
  "date": "2018-08-26",
  "open": "133.39",
  "high": "134.66",
  "low": "132.10",
  "close": "132.25"
}, {
  "date": "2018-08-27",
  "open": "130.99",
  "high": "132.41",
  "low": "126.63",
  "close": "126.82"
}, {
  "date": "2018-08-28",
  "open": "129.88",
  "high": "134.18",
  "low": "129.54",
  "close": "134.08"
}, {
  "date": "2018-08-29",
  "open": "132.67",
  "high": "138.25",
  "low": "132.30",
  "close": "136.25"
}, {
  "date": "2018-08-30",
  "open": "139.49",
  "high": "139.65",
  "low": "137.41",
  "close": "138.48"
}, {
  "date": "2018-09-03",
  "open": "139.94",
  "high": "145.73",
  "low": "139.84",
  "close": "144.16"
}, {
  "date": "2018-09-04",
  "open": "144.97",
  "high": "145.84",
  "low": "136.10",
  "close": "136.76"
}, {
  "date": "2018-09-05",
  "open": "135.56",
  "high": "137.57",
  "low": "132.71",
  "close": "135.01"
}, {
  "date": "2018-09-06",
  "open": "132.01",
  "high": "132.30",
  "low": "130.00",
  "close": "131.77"
}, {
  "date": "2018-09-09",
  "open": "136.99",
  "high": "138.04",
  "low": "133.95",
  "close": "136.71"
}, {
  "date": "2018-09-10",
  "open": "137.90",
  "high": "138.30",
  "low": "133.75",
  "close": "135.49"
}, {
  "date": "2018-09-11",
  "open": "135.99",
  "high": "139.40",
  "low": "135.75",
  "close": "136.85"
}, {
  "date": "2018-09-12",
  "open": "138.83",
  "high": "139.00",
  "low": "136.65",
  "close": "137.20"
}, {
  "date": "2018-09-13",
  "open": "136.57",
  "high": "138.98",
  "low": "136.20",
  "close": "138.81"
}, {
  "date": "2018-09-16",
  "open": "138.99",
  "high": "140.59",
  "low": "137.60",
  "close": "138.41"
}, {
  "date": "2018-09-17",
  "open": "139.06",
  "high": "142.85",
  "low": "137.83",
  "close": "140.92"
}, {
  "date": "2018-09-18",
  "open": "143.02",
  "high": "143.16",
  "low": "139.40",
  "close": "140.77"
}, {
  "date": "2018-09-19",
  "open": "140.15",
  "high": "141.79",
  "low": "139.32",
  "close": "140.31"
}, {
  "date": "2018-09-20",
  "open": "141.14",
  "high": "144.65",
  "low": "140.31",
  "close": "144.15"
}, {
  "date": "2018-09-23",
  "open": "146.73",
  "high": "149.85",
  "low": "146.65",
  "close": "148.28"
}, {
  "date": "2018-09-24",
  "open": "146.84",
  "high": "153.22",
  "low": "146.82",
  "close": "153.18"
}, {
  "date": "2018-09-25",
  "open": "154.47",
  "high": "155.00",
  "low": "151.25",
  "close": "152.77"
}, {
  "date": "2018-09-26",
  "open": "153.77",
  "high": "154.52",
  "low": "152.32",
  "close": "154.50"
}, {
  "date": "2018-09-27",
  "open": "153.44",
  "high": "154.60",
  "low": "152.75",
  "close": "153.47"
}, {
  "date": "2018-09-30",
  "open": "154.63",
  "high": "157.41",
  "low": "152.93",
  "close": "156.34"
}, {
  "date": "2018-10-01",
  "open": "156.55",
  "high": "158.59",
  "low": "155.89",
  "close": "158.45"
}, {
  "date": "2018-10-02",
  "open": "157.78",
  "high": "159.18",
  "low": "157.01",
  "close": "157.92"
}, {
  "date": "2018-10-03",
  "open": "158.00",
  "high": "158.08",
  "low": "153.50",
  "close": "156.24"
}, {
  "date": "2018-10-04",
  "open": "158.37",
  "high": "161.58",
  "low": "157.70",
  "close": "161.45"
}, {
  "date": "2018-10-07",
  "open": "163.49",
  "high": "167.91",
  "low": "162.97",
  "close": "167.91"
}, {
  "date": "2018-10-08",
  "open": "170.20",
  "high": "171.11",
  "low": "166.68",
  "close": "167.86"
}, {
  "date": "2018-10-09",
  "open": "167.55",
  "high": "167.88",
  "low": "165.60",
  "close": "166.79"
}, {
  "date": "2018-10-10",
  "open": "169.49",
  "high": "171.88",
  "low": "153.21",
  "close": "162.23"
}, {
  "date": "2018-10-11",
  "open": "163.01",
  "high": "167.28",
  "low": "161.80",
  "close": "167.25"
}, {
  "date": "2018-10-14",
  "open": "167.98",
  "high": "169.57",
  "low": "163.50",
  "close": "166.98"
}, {
  "date": "2018-10-15",
  "open": "165.54",
  "high": "170.18",
  "low": "165.15",
  "close": "169.58"
}, {
  "date": "2018-10-16",
  "open": "172.69",
  "high": "173.04",
  "low": "169.18",
  "close": "172.75"
}];

export default AmChartSample;

  • 上記ソースコードの通りだと以下のように 休日などのデータがない部分に余計な隙間がある。 image.png
  • dateAxis.skipEmptyPeriods = true;にすると以下のように image.png 投資関係やってる人からしたらこの表示方法がデフォルトのはず。
    ちなみに、このオプションは以下の記事で見つけました。
    Amchart (three) candle chart

参考情報

番外編

3. candlestickと一緒にscatterなどを表示させる

当初筆者がやりたかったことはこれです( ・´ー・`)ドヤッ

image.png

candlestickだけでなく、LineやScatterが描画されています!

ソースコード

好き勝手いじっているのでわかりにくくなっているかもしれませんが、ご容赦ください(o*。_。)oペコッ

ソースコードはここをクリックして確認
src/AmChartSample.js
import React, { Component } from 'react';
import * as am4core from '@amcharts/amcharts4/core';
import * as am4charts from '@amcharts/amcharts4/charts';
import am4themes_animated from '@amcharts/amcharts4/themes/animated';

am4core.useTheme(am4themes_animated);


class AmChartSample extends Component {
  componentDidMount() {
    this.chart = prepareChart();
  }

  componentWillUnmount() {
    if (this.chart) {
      this.chart.dispose();
    }
  }

  render() {
    return (
      <div id='chartdiv' style={{ width: '100%', height: '500px' }}></div>
    );
  }
}

function prepareChart() {
  // ... chart code goes here ...
  const chart = am4core.create('chartdiv', am4charts.XYChart);
  chart.cursor = new am4charts.XYCursor();
  chart.dateFormatter.inputDateFormat = 'yyyy-MM-dd';
  chart.paddingRight = 20;

  const dateAxis = chart.xAxes.push(new am4charts.DateAxis());
  dateAxis.renderer.grid.template.location = 0;
  dateAxis.renderer.minGridDistance = 60;
  dateAxis.skipEmptyPeriods = true;

  const valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
  valueAxis.tooltip.disabled = true;
  valueAxis.renderer.minWidth = 35;

  const candlestick = addCandlestick(chart)
  addLineSample(chart);
  addScatterSample(chart)

  const scrollbarX = new am4charts.XYChartScrollbar();
  scrollbarX.series.push(candlestick);
  chart.scrollbarX = scrollbarX;
  // chart.scrollbarX = new am4core.Scrollbar();

  return chart;
}

function addCandlestick(chart) {
  const candlestick = chart.series.push(new am4charts.CandlestickSeries());
  candlestick.dataFields.dateX = 'date';
  candlestick.dataFields.valueY = 'close';
  candlestick.dataFields.openValueY = 'open';
  candlestick.dataFields.lowValueY = 'low';
  candlestick.dataFields.highValueY = 'high';
  candlestick.tooltipText = 'Open: [bold]{openValueY.value}[/]\nLow: [bold]{lowValueY.value}[/]\nHigh: [bold]{highValueY.value}[/]\nClose: [bold]{valueY.value}[/]';

  chart.data = candleData;

  return candlestick;
}

function addLineSample(chart) {
  const lineSample = chart.series.push(new am4charts.LineSeries());
  lineSample.dataFields.dateX = 'value';
  lineSample.dataFields.valueY = 'value2';
  lineSample.strokeWidth = 2
  lineSample.stroke = chart.colors.getIndex(3);
  lineSample.strokeOpacity = 0.7;
  lineSample.data = [
    { 'value': '2018-08-05', 'value2': 140 },
    { 'value': '2018-08-26', 'value2': 170 }
  ];
}

function addScatterSample(chart) {
  const lineSample = chart.series.push(new am4charts.LineSeries());
  lineSample.dataFields.dateX = 'value';
  lineSample.dataFields.valueY = 'value2';
  lineSample.strokeWidth = 2
  lineSample.stroke = chart.colors.getIndex(3);
  lineSample.strokeOpacity = 0.0;
  lineSample.data = [
    { 'value': '2018-08-08', 'value2': 140 },
    { 'value': '2018-09-03', 'value2': 150 }
  ];

  // Add a bullet
  let bullet = lineSample.bullets.push(new am4charts.Bullet());
  // Add a triangle to act as am arrow
  let arrow = bullet.createChild(am4core.Triangle);
  arrow.horizontalCenter = "middle";
  arrow.verticalCenter = "middle";
  arrow.strokeWidth = 0;
  arrow.fill = chart.colors.getIndex(0);
  arrow.direction = "top";
  arrow.width = 12;
  arrow.height = 12;
}

const candleData = [{
  'date': '2018-08-01',
  'open': '136.65',
  'high': '136.96',
  'low': '134.15',
  'close': '136.49'
}, {
  'date': '2018-08-02',
  'open': '135.26',
  'high': '135.95',
  'low': '131.50',
  'close': '131.85'
}, {
  'date': '2018-08-05',
  'open': '132.90',
  'high': '135.27',
  'low': '128.30',
  'close': '135.25'
}, {
  'date': '2018-08-06',
  'open': '134.94',
  'high': '137.24',
  'low': '132.63',
  'close': '135.03'
}, {
  'date': '2018-08-07',
  'open': '136.76',
  'high': '136.86',
  'low': '132.00',
  'close': '134.01'
}, {
  'date': '2018-08-08',
  'open': '131.11',
  'high': '133.00',
  'low': '125.09',
  'close': '126.39'
}, {
  'date': '2018-08-09',
  'open': '123.12',
  'high': '127.75',
  'low': '120.30',
  'close': '125.00'
}, {
  'date': '2018-08-12',
  'open': '128.32',
  'high': '129.35',
  'low': '126.50',
  'close': '127.79'
}, {
  'date': '2018-08-13',
  'open': '128.29',
  'high': '128.30',
  'low': '123.71',
  'close': '124.03'
}, {
  'date': '2018-08-14',
  'open': '122.74',
  'high': '124.86',
  'low': '119.65',
  'close': '119.90'
}, {
  'date': '2018-08-15',
  'open': '117.01',
  'high': '118.50',
  'low': '111.62',
  'close': '117.05'
}, {
  'date': '2018-08-16',
  'open': '122.01',
  'high': '123.50',
  'low': '119.82',
  'close': '122.06'
}, {
  'date': '2018-08-19',
  'open': '123.96',
  'high': '124.50',
  'low': '120.50',
  'close': '122.22'
}, {
  'date': '2018-08-20',
  'open': '122.21',
  'high': '128.96',
  'low': '121.00',
  'close': '127.57'
}, {
  'date': '2018-08-21',
  'open': '131.22',
  'high': '132.75',
  'low': '130.33',
  'close': '132.51'
}, {
  'date': '2018-08-22',
  'open': '133.09',
  'high': '133.34',
  'low': '129.76',
  'close': '131.07'
}, {
  'date': '2018-08-23',
  'open': '130.53',
  'high': '135.37',
  'low': '129.81',
  'close': '135.30'
}, {
  'date': '2018-08-26',
  'open': '133.39',
  'high': '134.66',
  'low': '132.10',
  'close': '132.25'
}, {
  'date': '2018-08-27',
  'open': '130.99',
  'high': '132.41',
  'low': '126.63',
  'close': '126.82'
}, {
  'date': '2018-08-28',
  'open': '129.88',
  'high': '134.18',
  'low': '129.54',
  'close': '134.08'
}, {
  'date': '2018-08-29',
  'open': '132.67',
  'high': '138.25',
  'low': '132.30',
  'close': '136.25'
}, {
  'date': '2018-08-30',
  'open': '139.49',
  'high': '139.65',
  'low': '137.41',
  'close': '138.48'
}, {
  'date': '2018-09-03',
  'open': '139.94',
  'high': '145.73',
  'low': '139.84',
  'close': '144.16'
}, {
  'date': '2018-09-04',
  'open': '144.97',
  'high': '145.84',
  'low': '136.10',
  'close': '136.76'
}, {
  'date': '2018-09-05',
  'open': '135.56',
  'high': '137.57',
  'low': '132.71',
  'close': '135.01'
}, {
  'date': '2018-09-06',
  'open': '132.01',
  'high': '132.30',
  'low': '130.00',
  'close': '131.77'
}, {
  'date': '2018-09-09',
  'open': '136.99',
  'high': '138.04',
  'low': '133.95',
  'close': '136.71'
}, {
  'date': '2018-09-10',
  'open': '137.90',
  'high': '138.30',
  'low': '133.75',
  'close': '135.49'
}, {
  'date': '2018-09-11',
  'open': '135.99',
  'high': '139.40',
  'low': '135.75',
  'close': '136.85'
}, {
  'date': '2018-09-12',
  'open': '138.83',
  'high': '139.00',
  'low': '136.65',
  'close': '137.20'
}, {
  'date': '2018-09-13',
  'open': '136.57',
  'high': '138.98',
  'low': '136.20',
  'close': '138.81'
}, {
  'date': '2018-09-16',
  'open': '138.99',
  'high': '140.59',
  'low': '137.60',
  'close': '138.41'
}, {
  'date': '2018-09-17',
  'open': '139.06',
  'high': '142.85',
  'low': '137.83',
  'close': '140.92'
}, {
  'date': '2018-09-18',
  'open': '143.02',
  'high': '143.16',
  'low': '139.40',
  'close': '140.77'
}, {
  'date': '2018-09-19',
  'open': '140.15',
  'high': '141.79',
  'low': '139.32',
  'close': '140.31'
}, {
  'date': '2018-09-20',
  'open': '141.14',
  'high': '144.65',
  'low': '140.31',
  'close': '144.15'
}, {
  'date': '2018-09-23',
  'open': '146.73',
  'high': '149.85',
  'low': '146.65',
  'close': '148.28'
}, {
  'date': '2018-09-24',
  'open': '146.84',
  'high': '153.22',
  'low': '146.82',
  'close': '153.18'
}, {
  'date': '2018-09-25',
  'open': '154.47',
  'high': '155.00',
  'low': '151.25',
  'close': '152.77'
}, {
  'date': '2018-09-26',
  'open': '153.77',
  'high': '154.52',
  'low': '152.32',
  'close': '154.50'
}, {
  'date': '2018-09-27',
  'open': '153.44',
  'high': '154.60',
  'low': '152.75',
  'close': '153.47'
}, {
  'date': '2018-09-30',
  'open': '154.63',
  'high': '157.41',
  'low': '152.93',
  'close': '156.34'
}, {
  'date': '2018-10-01',
  'open': '156.55',
  'high': '158.59',
  'low': '155.89',
  'close': '158.45'
}, {
  'date': '2018-10-02',
  'open': '157.78',
  'high': '159.18',
  'low': '157.01',
  'close': '157.92'
}, {
  'date': '2018-10-03',
  'open': '158.00',
  'high': '158.08',
  'low': '153.50',
  'close': '156.24'
}, {
  'date': '2018-10-04',
  'open': '158.37',
  'high': '161.58',
  'low': '157.70',
  'close': '161.45'
}, {
  'date': '2018-10-07',
  'open': '163.49',
  'high': '167.91',
  'low': '162.97',
  'close': '167.91'
}, {
  'date': '2018-10-08',
  'open': '170.20',
  'high': '171.11',
  'low': '166.68',
  'close': '167.86'
}, {
  'date': '2018-10-09',
  'open': '167.55',
  'high': '167.88',
  'low': '165.60',
  'close': '166.79'
}, {
  'date': '2018-10-10',
  'open': '169.49',
  'high': '171.88',
  'low': '153.21',
  'close': '162.23'
}, {
  'date': '2018-10-11',
  'open': '163.01',
  'high': '167.28',
  'low': '161.80',
  'close': '167.25'
}, {
  'date': '2018-10-14',
  'open': '167.98',
  'high': '169.57',
  'low': '163.50',
  'close': '166.98'
}, {
  'date': '2018-10-15',
  'open': '165.54',
  'high': '170.18',
  'low': '165.15',
  'close': '169.58'
}, {
  'date': '2018-10-16',
  'open': '172.69',
  'high': '173.04',
  'low': '169.18',
  'close': '172.75'
}];

export default AmChartSample;

番外編の参考

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

初心者がReact学習歴1週間でWebアプリ作成に挑戦してみた

はじめに

Reactの学習兼ねてWebアプリを作成してみました。
タイトル通り、Reactのインプット期間は1週間程です。
(仕事してるので時間でいうと恐らく数日程度です)

今回の記事では、インプット学習歴1週間でもなんとか作れた話をお伝えします。

※挑戦する段階で1週間程度のインプットで開発を始めたという話です。
開発期間は余裕で1ヶ月以上かかりました。

私についてですが、今年6月からフロントエンドエンジニアを目指して独学中です。
アプリを作り始めたスキルレベルは、主にHTML/CSS/JavaScriptを勉強しておりました。

0から作ったことはないので、今回が初めてのアプリ作成になります。

見ていただくとわかる通り比較的シンプルなアプリなのですが、
私にとっては全く簡単ではなく、、
でも試行錯誤しながら何とか作成できました!

今回作ったもの

ログイン機能付きの収支管理アプリです。
いわゆる家計簿アプリ的な物です。
毎月の収入と支出をリスト化し月の残高を表示します。

メイン画面
mainpic.png

サインアップ画面
Page-Signup.png

ログイン画面
Page-Signin.png

操作動画はTwitterにアップしてます。(画質粗くなってしまいましたが)

このアプリを作った背景

  • Reactを理解するため
  • 身近な人を助けたかったため

母が未だに家計簿をノートで管理していていました。(驚愕です)
管理が大変そうで、「便利」を作ってあげたいと思ったからですね。
「家計簿アプリどれがいいかよくわからない。表とかいらない」ということで、、
私がシンプルな物を作ってあげようと思いました!

あと、クレジットカードについて、金額がリアルタイムに更新されず困っていたんですね。
カード会社によってはありますよね、時差生じるの。

なのでこちらは、家計簿として使ってもOK、収支管理として使ってもOK、というシンプルで自由なアプリです。
スマフォで入力することが多いと思うので、ホームiconとかレスポンシブにも勿論対応してます。

実装した機能

  • データを保存/取り出し
  • ユーザー登録
  • ログイン機能
  • 表の代わりに支出割合を%で表示
  • 月ごとに管理(実装中)

環境・使用技術

  • node (version 14.8.0)
  • React(version 16.13.1)
    • Router
    • クラスの代わりにHooks
  • Firebase
    • Authentication
    • Cloud Firestore
    • Hosting
  • CSS/material-ui(スタイル)

ソース管理

Git/Githubを使いました。
個人開発ですが、チームの開発を想定して毎回issueを作り、branchを切って作業を進めておりました。

今回のソースはこちら。
https://github.com/kana-wwib/My-budget-app

まだ追加機能とか修正箇所がありますので、コードは変わっていきます。

大変だったこと

細かいことあげると本当にキリがないくらい大変なことだらけですが、、
例えば円のカンマ区切りに苦労したり、リストが順番おかしくなったり。

でもとりあえず、大きく3つに分けて書きます。

Reactの書き方

  • hooksを使う
  • propsの渡し方

基本のキだと思うんですが、超初心者からすると苦労しました。

まず、hooksに関しては、当初インプット学習してた時classの書き方だったんですね。
hooksを全然知らなかったので、それって何?状態から調べました。

今回使用しているのは、useState useEffect useContext です。
classに慣れていたレベルでも勿論ないですが、hooksで一から学び直しながら作るのは大変でしたね。

でも結果、hooksの方が圧倒的にシンプルで書きやすかったです!
今はもうhooksの方が慣れ親しんでます。(今後classで作ることは無いとは思ってます)

propsについては、これもインプットした時分かっていたつもりが多分あまり分かってなかったんですね。
実際に自分で書こうとすると、どうやって渡すんだっけ?という状況でした。

ステートの書く場所も作りながら変わりましたね。やっぱりメインで書かないとだめだ、みたいな。

useContextを認証系で使ってますが、それ以外は基本親→子供に渡すという基本を作りながら理解しました。

あと余談ですが、Reactでは、データは常に単一方向で、Vue.jsは双方向性って最初何のこと?と思ってましたが、こういうことかと実感しました。

Firebaseのデータベース

データを保存/取り出しをするのに、今回Firebaseを使いました。
書籍で学んだ時、一応Firebase使ったんですが、Realtime Databaseだったんですね。

今回のアプリではCloud Firestoreの方を使ったのでやり方が違いました。
ちなみにこちらを選んだ理由は、最新でより高機能だからです。

私はそもそもデータベースについて知識が皆無で、SQL?NoSQL?というレベルでした。
※FirebaseはNoSQL

Firebaseの公式の説明や動画などを見て、どのような形でデータが保存されるのかを試行錯誤しながら進めました。
もちろん、公式以外もいっぱい参考にしてます。

最終的なデータの形はこうなっています。
最初は収入と支出分けなくてもと思いましたが、後々の計算の為分けて保存してます。
collection.png

作り始めの時は、データとログインのユーザーをどうやって繋げるんだ...と思う程未知でしたが、
色々調べてログインしたユーザはuidを使ってうまく個人のデータに分けることができました。

スタイル

スタイルは今回メインでCSSを使ってます。
独学初めの当初に勉強してましたが、最近はずっとJavaScriptを集中的に学んでいたので、すっかり忘れていました...

デザインの知識は皆無なので、前にチュートリアル でやっていた見た目のデザインを真似しました。
あとは色々な見やすい色合いや配置等を調べて参考にしました。
例えばログイン画面ですら、「あれ、普通どんなんだっけ?」という状況でした。

何とか調べながらいい感じにできたわけですが、追加機能をつけるたびにスタイルも調整するという手間があったり。
おそらくCSSメインで書かなければもっと楽だったのかなと思うので、material-uiもっと使いこなせるようになろうと思いましたね。

参考記事・動画

教材としては、色々な記事やYoutubeを参考にしました。

何より例があるとより分かりやすかったです!

色々読みまくったので全部は取り上げませんが、分かりやすくて印象に残ってる分を抜粋します。

追加予定機能(実装中含む)

  • 月ごとのデータ管理
  • 項目の検索機能(月ごと)
  • レシートから料金を文字化してinput(これはできればくらい)

作ってみた感想

よく皆さん言うことですが、アウトプットしながらの勉強は本当に効果があります。
私の場合最初にインプットで学んだ内容は、もはや結局使わず仕舞いでした。

たかがReact1週間の学習歴でWebアプリの作成に挑戦したのですが、
結局は作りながら学ぶので、インプット期間は最小限で何とかなります。
インプットに時間をかけても意味がないというのを本当に実感しました。

最初学んでる時多少分かった気になっていたんですが、、、
実際に作ってみることできちんと理解していないことがわかりました。(私の場合は)
自分の手で実際に作ることで、きちんと深く理解ができたと思います。

あと、思い描いた内容がきちんと反映されて動くのは、最高に嬉しかったですね。
エラーと葛藤してた内容とか、実装方法に苦労してた内容とかは、特に感じました。

まだ追加したい機能もありますし、一つ一つ時間もかかりますが、
また新たなアプリを作りながら学習していこうと思います!

最後に、今回は一つ一つの機能の実装方法について触れていないので、
次回の記事で初心者のための初心者による記事という感じで、解説していこうと思います。
ご興味ある方は是非ご覧くださいませ。

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

React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で

Reactにフックが採り入れられて、関数コンポーネントに状態をもたせられるようになりました。

「フックとは、関数コンポーネントにstateやライフサイクルといった Reactの機能を"接続する(hook into)"ための関数です」(「要するにフックとは?」)。さらに、フックを独自につくって、コンポーネントからロジックを切り出すこともできます。そうすれば、コンポーネントのコードがすっきり見やすくなるとともに、そのカスタムフックを使い回すこともできるのです。

自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。
(「独自フックの作成」より)

本稿は簡単なカウンターの作例をとおして、フックの役割や考え方について解説します。

Create React Appでアプリケーションのひな形をつくる

まず、Reactアプリケーションのひな形は、Create React Appでつくりましょう。コマンドラインツールでnpx create-react-appにつづけて、アプリケーション名(今回はreact-custom-hook)を打ち込んでください。

npx create-react-app react-custom-hook

アプリケーション名でつくられたディレクトリに切り替えて(cd react-custom-hook)、コマンドyarn startでひな形アプリケーションのページがローカルホスト(http://localhost:3000/)で開くはずです。

useStateとプロパティ(props)でカウンターをつくる

まずは、カスタムフックは用いず、useStateとプロパティ(props)によりカウンターをつくりました(コード001)。

アプリケーションのモジュールsrc/App.jsuseStateで状態変数(count)を定め、関数としてはカウンターの減算(decrement())と加算(increment())が備わっています。それらをプロパティ(counter)として受け取るのが、このあと定めるカウンター表示のコンポーネントCounterDisplayです。

コード001■useStateとプロパティを用いたアプリケーションモジュール

src/App.js
import React, { useState } from 'react';
import CounterDisplay from './CounterDisplay';
import './App.css';

const initialCount = 0;
function App() {
    const [count, setCount] = useState(initialCount);
    const decrement = () => setCount(count - 1);
    const increment = () => setCount(count + 1);
    return (
    <div className="App">
      <CounterDisplay counter={{ count, decrement, increment }} />
    </div>
    );
}

export default App;

カウンター表示のモジュールsrc/CounterDisplay.jsは、アプリケーションから受け取ったプロパティ(counter)により、カウンタの値(counter.count)表示と減算(counter.decrement)・加算(counter.increment)の処理を行います(コード002)。

コード002■カウンター表示のモジュール

src/CounterDisplay.js
import React from "react";

const CounterDisplay = ({ counter }) => {
    return (
        <div>
            <button onClick={counter.decrement}>-</button>
            <span>{counter.count}</span>
            <button onClick={counter.increment}>+</button>
        </div>
    );
}
export default CounterDisplay;

これで簡単なカウンターができ上がりました(図001)。ただカウンターのカウントアップ・ダウンをするだけで、アプリケーションモジュールには、状態を使って何か行うという処理がありません。あとで加わるという想定にして、今回の作例からは省きました。また、CSS(src/index.csssrc/App.css)は、基本的なフォントや余白の設定のみです。確かめたい方は、最後に掲げるCodeSandboxの作例(サンプル001)をご覧ください。

図001■でき上がったカウンター

2009001_001.png

アプリケーションのロジックをカスタムフックに切り出す

アプリケーションモジュールsrc/App.jsのロジック、つまり状態変数とその処理関数を、このあと定めるカスタムフックに切り出しましょう。

src/App.js
// import React, { useState } from 'react';
import React from 'react';

// const initialCount = 0;
function App() {
    /* カスタムフックに切り出す
    const [count, setCount] = useState(initialCount);
    const decrement = () => setCount(count - 1);
    const increment = () => setCount(count + 1);
    */

}

つぎのコード003が、カウンターのカスタムフックのモジュール(src/useCounter.js)です。フックの名前はuseではじめるお約束になっています。状態が備えられ、フックも使えるのは、関数コンポーネントと同じです。違いとしては、JSXの要素を返さなくて構いません。戻り値は、カウンターの値(count)と減算(decrement())・加算(incfrement())の関数を収めたオブジェクトとしました。

コード003■カウンターのカスタムフック

src/useCounter.js
import { useState } from 'react';

export const useCounter = (initialCount = 0) => {
    const [count, setCount] = useState(initialCount);
    const decrement = () => setCount(count - 1);
    const increment = () => setCount(count + 1);
    return { count, decrement, increment };
};

カスタムフックを使う

アプリケーションモジュール(src/App.js)はカスタムフック(useCounter)の呼び出しにより、カウンターの状態を操作するための参照(countdecrementおよびincrement)が得られます。前掲コード001とプロパティ名を揃えましたので、カウンター表示のコンポーネント(CounterDisplay)に渡すプロパティやモジュール(src/CounterDisplay.js)のコードは書き替える必要がありません。

src/App.js
import { useCounter } from './useCounter';

function App() {
    const { count, decrement, increment } = useCounter();
    return (
    <div className="App">
      <CounterDisplay counter={{ count, decrement, increment }} />
    </div>
    );
}

アプリケーションモジュール(src/App.js)の記述全体は、つぎのコード004のとおりです。ロジックを切り離したので、表示に専念することになりました。作例をCodeSandboxに公開します(サンプル001)。

コード004■ロジックを切り離したアプリケーション

src/App.js
import React from 'react';
import { useCounter } from './useCounter';
import CounterDisplay from './CounterDisplay';
import './App.css';

function App() {
    const { count, decrement, increment } = useCounter();
    return (
    <div className="App">
      <CounterDisplay counter={{ count, decrement, increment }} />
    </div>
    );
}

export default App;

サンプル001■カスタムフックを使ったカウンター

2009001_002.png
>> CodeSandboxへ

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

Next.jsにjestとenzymeを導入(next/babel使用)

Next.jsにjestとenzymeを導入(next/babel使用)

以前、Next.jsにjestとenzymeを導入するという記事を書きました。

上記の手順でjestの実行はできたのですが、yarn devでアプリ起動するとなにやらbabelに関するエラーが。。

どうやらNext.js起動すると追加したbabelの設定ファイルの方が読み込まれて、babelのエラーが出てしまっているよう。

そこで、jestで使うbabelをnext.jsのbabelに変更したところ、よりすっきりした設定になったのでメモ。

next.jsのbabelが使える

next.jsにはデフォルトでbabelが入っており、これがjsxのトランスパイルなどjestにも適用できることが分かりました。
こちらの方がスッキリとした手順・設定で構築できます。

jestインストール

$ npm install --save-dev jest 

jest設定ファイルを生成

$ jest --init
command not found: jest の場合

以下を実行します。

./node_modules/.bin/jest --init   

Enzymeインストール

yarn add --dev enzyme enzyme-adapter-react-16

Enzymeの利用時は一度だけEnzyme.configure()を呼ぶ必要があるため、下記のスクリプトを追加。

jest.setup.js
import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({ adapter: new Adapter() });

Jestのテスト前に実行されるようにする。

jest.config.js
module.exports = {
  // ...
  setupFiles: ['./jest.setup.js'],
  // ...
}

babel.config.jsを設定

module.exports = {
    "presets": ["next/babel"],
};   

テストファイルのignore

Cypressを導入しており、jest実行でcypressのspecも読まれてしまうので、ignore設定をしました。

jest.config.js
...  
testPathIgnorePatterns: [
    "/node_modules/",
    "/cypress/"
],
...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptでReact Hooksに入門するチュートリアル

はじめに

本投稿の背景と目的

React HooksはReactアプリケーションを開発する際のファーストチョイスになっていると言っても過言ではありません。
Reactの初学者がHooksを学ぶ際に、一通りの情報は公式ドキュメントにまとまっているのですが、従来の公式チュートリアルのHooks版があったらいいのにな〜、と思いました。

というわけで、React Hooksに入門するためのチュートリアルを提供することが本稿の目的です。

なお、このチュートリアルではフックの中でも最も基本的かつ重要な useStateフックとuseEffectフックを習得対象とします。まずはこの2つを覚えれば、ちょっとしたReactアプリケーションの開発を始めることが可能です。

対象とする読者

  • React Hooks以前のReactコンポーネント開発の基礎は理解しており、これからReact Hooksを学びたい方

Hooksを使わない、従来のクラスコンポーネント・関数コンポーネントによる開発方法を復習したい方は、公式チュートリアルや、私が以前に書いた記事などを参考にして頂ければと思います。

開発環境

本チュートリアルは、CodeSandboxの環境を使ってブラウザ上でステップ・バイ・ステップでアプリケーションを開発していく流れとなっています。
ブラウザ上ではなくローカル環境で進めたいという方は、 付録B ローカル開発環境のセットアップ を参考にして環境を構築ください。

動作確認済みの環境

アプリケーションの動作確認は、CodeSandboxの他、以下の環境で行いました。

  • Mac OS X 10.15.6
  • node v11.10.1
  • npm 6.14.5
  • yarn 1.19.1
  • react 16.13.1
  • typescript 3.7.2

これからつくるもの

このチュートリアルでは、これから読みたい・買いたい本をストックしておくちょっとしたWebアプリケーションを作成します。
書籍情報はGoogle Books APIsを利用してAjaxで取得します。

react_00.png

Qiitaに動画をアップロードできなかったので、動かしている様子は個人ブログへアップしました。

最終的な結果をここで確認することができます:最終結果
チュートリアルを進める前に実際に画面を操作して動作を把握しておくことをお勧めします。

ステップ1:書籍のリストを表示する

スターターコードを確認する

ブラウザからスターターコードを開いてください。以下のようにCodeSandboxの画面が表示されるはずです。
保存時に自動でフォークされて自分専用のサンドボックスとなりますので、このあと自由にファイルの追加や編集を行ってください。

react_01.png

左側のペインにはサンドボックスの情報やエクスプローラが表示されています。中央のペインでコード編集を行い、右側のペインでリアルタイムに表示を確認することができます。
スターターには以下のファイルを事前に準備済みです。

ファイル 説明
App.css アプリケーション共通で利用するスタイルを定義(ReactにおけるCSSの管理方法は本稿範囲外なので、全てのスタイルをここで定義している)
App.tsx アプリケーションのメインReactコンポーネント
BookDescription.ts APIで取得する書籍情報の型を定義
BookToRead.ts アプリケーションで保管する書籍情報の型を定義
index.css index.tsxで利用するスタイルを定義
index.tsx エントリポイントとなるReactコンポーネント
package.json npmの構成ファイル
tsconfig.json TypeScriptの構成ファイル

上記のうち、App.tsx以外のファイルはチュートリアルの中で変更することはありません。

ダミーデータをリスト表示する

App.tsxには予めダミーの書籍データを用意してあるので、まずはこれをリスト形式で表示するコードを書きましょう。

App.tsx
const dummyBooks: BookToRead[] = [
  {
    id: 1,
    title: "はじめてのReact",
    authors: "ダミー",
    memo: ""
  },
  {
    id: 2,
    title: "React Hooks入門",
    authors: "ダミー",
    memo: ""
  },
  {
    id: 3,
    title: "実践Reactアプリケーション開発",
    authors: "ダミー",
    memo: ""
  }
];

CodeSandBoxのエクスプローラから、srcフォルダの下に新規ファイルBookRow.tsxを作成してください。
まずはインポート。

BookRow.tsx
import React from "react";
import { BookToRead } from "./BookToRead";

次に、propsの型を定義します。

BookRow.tsx
type BookRowProps = {
  book: BookToRead;
  onMemoChange: (id: number, memo: string) => void;
  onDelete: (id: number) => void;
};

BookToRead型の書籍情報(book)のほか、メモ項目の変更イベントのコールバック、書籍削除イベントのコールバックを持たせておきます。
そして、関数コンポーネントの本体を定義してエクスポートします。

const BookRow = (props: BookRowProps) => {
  const { title, authors, memo } = props.book;

  const handleMemoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    props.onMemoChange(props.book.id, e.target.value);
  };

  const handleDeleteClick = () => {
    props.onDelete(props.book.id);
  };

  return (
    <div className="book-row">
      <div title={title} className="title">
        {title}
      </div>
      <div title={authors} className="authors">
        {authors}
      </div>
      <input
        type="text"
        className="memo"
        value={memo}
        onChange={handleMemoChange}
      />
      <div className="delete-row" onClick={handleDeleteClick}>
        削除
      </div>
    </div>
  );
};

export default BookRow;

このコンポーネントは表示とイベント伝搬を行うのみで状態管理は必要としないため、React Hooksの出番はなく、通常の関数コンポーネントとして実装できます。つまり、propsで受け取ったプロパティを用いてレンダリングを行い、子コンポーネントでonChangeonClickなどのイベントが発生した際はpropsのプロパティを通じて親コンポーネントにイベントを伝搬します。

では次にApp.tsxから今作ったコンポーネントを利用して表示を行いましょう。

インポート文を追加します。

App.tsx
import BookRow from "./BookRow";

ダミーデータを各要素をJSX要素に変換して変数に格納しましょう。今はまだBookRowコンポーネントから発火されるイベントは無視します。

App.tsx
const App = () => {
  const bookRows = dummyBooks.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => {}}
        onDelete={(id) => {}}
      />
    );
  });

繰り返し出力するコンポーネントに対しては、key属性を付ける必要があることを思い出してください。
次に、コンポーネントの戻り値となるJSX要素内に展開されるように記述します(クラス名がmainsection要素配下)。

  return (
    <div className="App">
      <section className="nav">
        <h1>読みたい本リスト</h1>
        <div className="button-like">本を追加</div>
      </section>
      <section className="main">{bookRows}</section>
    </div>
  );

これで、以下のようにリスト表示されるようになったでしょう。

react_02.png

これでステップ1は終了です。この時点でのコードはこのようになっているはずです。

ステップ2:書籍の削除とメモ書きを実装する

このステップの内容は、ステップ1終了時の状態から続けて実装します。

useStateフックによる状態管理

書籍の削除やメモ書きの変更のイベントを拾い、画面に反映させるためには、書籍のリストをコンポーネントのステート変数として管理する必要があります。
従来のReactでは、そのためにはクラスコンポーネントを作成して、this.stateの中に状態を管理する必要がありました。React Hooksの導入以後は、関数コンポーネントにおいても状態管理の実現が可能となりました。

そのために用いるのがuseStateフックです。
実際にコードを書きながら、使い方を確認しましょう。

今回のステップでやりたいことは以下です。

  • dummyBooksの内容を初期状態とするステート変数を作成する
  • 書籍削除のイベントを拾って上記ステート変数を更新する
  • 同じくメモ書き変更のイベントを拾ってステート変数を更新する

順に実装していきましょう。

書籍のリストを状態管理する

まずはuseState関数をインポートします。

App.tsx
import React, { useState } from "react";

このuseState関数を利用して、ステート変数とその更新用関数を取得します。

App.tsx
const App = () => {
  const [books, setBooks] = useState(dummyBooks);

useState関数の引数には、そのステート変数の初期値を指定します。
上記のコード例だと、初めてApp関数コンポーネントが呼び出された際(初回のレンダリング時)にbooks変数に格納されているのはdummyBooksで定義したダミーの書籍データの配列となります。

ステート変数booksの内容を表示するようにコードを修正します(dummyBooks->books)。ブラウザに表示される結果が変わらないことを確認してください。

App.tsx
  const bookRows = books.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => {}}
        onDelete={(id) => {}}
      />
    );
  });

削除イベントのハンドリング

削除イベントのハンドラ関数を定義しましょう。書籍のIDを受け取り、該当する書籍を配列から削除します。
クラスコンポーネントにおけるstateの更新と同様、ステート変数の配列を直接操作するのではなく、新しい配列を生成して更新用関数に渡す点に注意してください。

App.tsx
  const [books, setBooks] = useState(dummyBooks);

  const handleBookDelete = (id: number) => {
    const newBooks = books.filter((b) => b.id !== id);
    setBooks(newBooks);
  };

ここでは、filter関数を使って、IDが一致する書籍を除外した配列を生成し、setBooks関数を通じてステート変数の更新を行います。
JSXを修正し、BookRowコンポーネントのonDelete属性で先程のハンドラを呼び出すようにします。

App.tsx
  const bookRows = books.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => {}}
        onDelete={(id) => handleBookDelete(id)}
      />
    );
  });

実際の画面から任意の行の削除ボタンをクリックし、行が消えるようになったことを確認してください。

メモ書き変更イベントのハンドリング

同様のイベントのハンドラ関数を定義します。

  const handleBookMemoChange = (id: number, memo: string) => {
    const newBooks = books.map((b) => {
      return b.id === id
        ? { ...b, memo: memo }
        : b;
    });
    setBooks(newBooks);
  }

booksに格納されている書籍データの配列のうち、IDが合致する要素はメモを更新した値を、それ以外の要素はそのままの値で新しい配列に格納します。

{ ...b, memo: memo }

上のコードは、bの各プロパティを展開し、memoプロパティだけを上書きした新しいオブジェクトを生成しています。

JSXを修正しイベントとハンドラの紐付けを行います。

App.tsx
  const bookRows = books.map((b) => {
    return (
      <BookRow
        book={b}
        key={b.id}
        onMemoChange={(id, memo) => handleBookMemoChange(id, memo)}
        onDelete={(id) => handleBookDelete(id)}
      />
    );
  });

書籍リストのメモ欄が編集できるようになったことを画面で確認しましょう。

これでステップ2は終了です。この時点でのコードはこのようになっているはずです。

ステップ3:書籍を検索して追加する

このステップの内容は、ステップ2終了時の状態から続けて実装します。

検索ダイアログ

書籍の検索はモーダルダイアログで実現するため、react-modalライブラリを利用します。
CodeSandboxには既に依存関係を登録済みです。ローカル開発環境用のスタータープロジェクトにも登録済みですが、もしcreate-react-appで一から環境を構築する場合は以下のようにライブラリを追加します。

$ yarn add react-modal
$ yarn add @types/react-modal

これから作成するのは以下のようなダイアログコンポーネントです。

react_03.png

各々の書籍情報をタイルっぽい形で表示するコンポーネントを先に作ります。

BookSearchItemコンポーネント

CodeSandboxのエクスプローラから、新しいファイルBookSearchItem.tsxを作成してください。

まずはインポートとpropsの型定義。

BookSearchItem.tsx
import React from "react";
import { BookDescription } from "./BookDescription";

type BookSearchItemProps = {
  description: BookDescription;
  onBookAdd: (book: BookDescription) => void;
};

BookDescription はAPIで取得した書籍情報のうち、タイトル、著者(群)、サムネイル画像(のURL)を保持する型です。それに加えて、サムネイル画像の右下にある「+」をクリックした際のイベントを拾うコールバック関数を含めたものが当コンポーネントのpropsとなります。

続いてコンポーネント本体となる関数を定義し、エクスポートします。

BookSearchItem.tsx
const BookSearchItem = (props: BookSearchItemProps) => {
  const { title, authors, thumbnail } = props.description;
  const handleAddBookClick = () => {
    props.onBookAdd(props.description);
  };
  return (
    <div className="book-search-item">
      <h2 title={title}>{title}</h2>
      <div className="authors" title={authors}>
        {authors}
      </div>
      {thumbnail ? <img src={thumbnail} alt="" /> : null}
      <div className="add-book" onClick={handleAddBookClick}>
        <span>+</span>
      </div>
    </div>
  );
};

export default BookSearchItem;

特に難しいところはないかと思うので説明は割愛します。

BookSearchDialogコンポーネント(基本部分)

検索ダイアログの基本部分から作っていきましょう。エクスプローラから新規ファイルBookSearchDialog.tsxを作成してください。

インポートとpropsの型定義です。コンポーネントのプロパティとしては、検索結果の表示最大件数(maxResults)と書籍追加イベントを拾うコールバック関数(onBookAdd)を持たせます。

BookSearchDialog.tsx
import React, { useState } from "react";
import { BookDescription } from "./BookDescription";
import BookSearchItem from "./BookSearchItem";

type BookSearchDialogProps = {
  maxResults: number;
  onBookAdd: (book: BookDescription) => void;
};

コンポーネント本体の関数を実装していきましょう。まずはuseState関数を使ってステート変数を定義します。

BookSearchDialog.tsx
const BookSearchDialog = (props: BookSearchDialogProps) => {
  const [books, setBooks] = useState([] as BookDescription[]);
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");

booksは書籍の検索結果を表す配列。初期値は空の配列です。
title authorは検索条件のタイトルおよび著者名。どちらも初期値は空文字列です。

次にイベントハンドラのコールバック関数。
タイトル、著者名のinput要素のonChangeイベントを拾い、それぞれのステート変数を更新します。

BookSearchDialog.tsx
  const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(e.target.value);
  };

  const handleAuthorInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setAuthor(e.target.value);
  };

実際にこれらのイベントが発火してsetXXXの関数呼び出しを通じてステート変数が変化すると、Reactはそれを検知してコンポーネントの再レンダリングを行います。
再レンダリングを行うということは即ちBookSearchDialog関数が呼び出されるということです。

例えばタイトルに「A」と入力した場合、setTitle(e.target.value) によって、ステート変数の値がAに更新されます。その後再レンダリングのためにBookSearchDialog関数が呼び出されると(初回レンダリング時と同様に)以下のコード行が実行されますが、このときuseStateが返すtitleの値はAになっています。

const [title, setTitle] = useState("");

このあたりのステート変数の更新と再レンダリングの仕組みは押さえておく必要があります。(なお、親コンポーネントから渡されるpropsの値が変更された場合も、子コンポーネントは再レンダリングされます)

さらに、検索ボタンのクリックイベントをハンドリングするコールバックも定義しましょう。(実際の検索処理は後で実装します)

BookSearchDialog.tsx
  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    // 検索実行
  };

書籍追加イベントに対するコールバックも実装しておきます。これは子のBookSearchItemで発火したイベントを親コンポーネントへ伝搬するだけです。

BookSearchDialog.tsx
  const handleBookAdd = (book: BookDescription) => {
    props.onBookAdd(book);
  };

最後にレンダリング処理です。
検索結果はBookSearchItemコンポーネントを配列の要素数だけ繰り返し出力します。各イベントとハンドラの紐付けを行いましょう。

 const bookItems = books.map((b, idx) => {
    return (
      <BookSearchItem
        description={b}
        onBookAdd={(b) => handleBookAdd(b)}
        key={idx}
      />
    );
  });

  return (
    <div className="dialog">
      <div className="operation">
        <div className="conditions">
          <input
            type="text"
            onChange={handleTitleInputChange}
            placeholder="タイトルで検索"
          />
          <input
            type="text"
            onChange={handleAuthorInputChange}
            placeholder="著者名で検索"
          />
        </div>
        <div className="button-like" onClick={handleSearchClick}>
          検索
        </div>
      </div>
      <div className="search-results">{bookItems}</div>
    </div>
  );

エクスポートも忘れないように。

BookSearchDialog.tsx
export default BookSearchDialog;

ちょっと実装内容が多かったので、現時点のSearchBookDialog.tsxのコード全量を載せておきます。

SearchBookDialog.tsx(全量)
import React, { useState } from "react";
import { BookDescription } from "./BookDescription";
import BookSearchItem from "./BookSearchItem";

type BookSearchDialogProps = {
  maxResults: number;
  onBookAdd: (book: BookDescription) => void;
};

const BookSearchDialog = (props: BookSearchDialogProps) => {
  const [books, setBooks] = useState([] as BookDescription[]);
  const [title, setTitle] = useState("");
  const [author, setAuthor] = useState("");

  const handleTitleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setTitle(e.target.value);
  };

  const handleAuthorInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setAuthor(e.target.value);
  };

  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    // 検索実行
  };

  const handleBookAdd = (book: BookDescription) => {
    props.onBookAdd(book);
  };

  const bookItems = books.map((b, idx) => {
    return (
      <BookSearchItem
        description={b}
        onBookAdd={(b) => handleBookAdd(b)}
        key={idx}
      />
    );
  });

  return (
    <div className="dialog">
      <div className="operation">
        <div className="conditions">
          <input
            type="text"
            onChange={handleTitleInputChange}
            placeholder="タイトルで検索"
          />
          <input
            type="text"
            onChange={handleAuthorInputChange}
            placeholder="著者名で検索"
          />
        </div>
        <div className="button-like" onClick={handleSearchClick}>
          検索
        </div>
      </div>
      <div className="search-results">{bookItems}</div>
    </div>
  );
};

export default BookSearchDialog;

App.tsxからのダイアログ表示

次に、「本を追加」ボタンクリックで検索ダイアログをモーダル表示するようにApp.tsxに手を加えます。

まずはインポートの追加と、react-modalを利用するための準備を行います。

App.tsx
import Modal from "react-modal";
import BookSearchDialog from "./BookSearchDialog";

Modal.setAppElement("#root");

const customStyles = {
  overlay: {
    backgroundColor: "rgba(0, 0, 0, 0.8)"
  },
  content: {
    top: "50%",
    left: "50%",
    right: "auto",
    bottom: "auto",
    marginRight: "-50%",
    padding: 0,
    transform: "translate(-50%, -50%)"
  }
};

Modal.setAppElementの呼び出しにより、モーダル表示時にオーバーレイで覆うDOM領域を指定します。
customStylesはモーダルダイアログおよびオーバーレイの外観のスタイル設定となります。ここでは上記の通りコード入力してください。

次にステート変数を追加します。

App.tsx
const App = () => {
  const [books, setBooks] = useState(dummyBooks);
  const [modalIsOpen, setModalIsOpen] = useState(false);

「モーダルダイアログが開いているかどうか」という画面モードをステート変数として持たせて切り替えを行います。最初は閉じていてほしいので、初期値をfalseにしています。

イベントハンドラを定義しましょう。

App.tsx
  const handleAddClick = () => {
    setModalIsOpen(true);
  };

  const handleModalClose = () => {
    setModalIsOpen(false);
  };

handleAddClickは「本を追加」ボタンのクリックに対するものなので、モーダルダイアログを開くためにsetModalIsOpentrueを指定します。
handleModalCloseはモーダルダイアログが開かれた状態で、ダイアログの領域外をクリックした際に呼び出されるものです。(falseを指定します)

JSXにコードを追加します。

App.tsx
  return (
    <div className="App">
      <section className="nav">
        <h1>読みたい本リスト</h1>
        <div className="button-like" onClick={handleAddClick}>
          本を追加
        </div>
      </section>
      <section className="main">{bookRows}</section>
      <Modal
        isOpen={modalIsOpen}
        onRequestClose={handleModalClose}
        style={customStyles}
      >
        <BookSearchDialog maxResults={20} onBookAdd={(b) => {}} />
      </Modal>
    </div>
  );

修正箇所は以下の2つです。

  • 「本を追加」のdivonClick属性を付与し、handleAddClickコールバック関数を紐付け
  • Modalコンポーネントを配置し、その子コンポーネントとして先ほど作成したBookSearchDialogコンポーネントを指定

これで、検索ダイアログがモーダル表示できるようになったはずです。(検索処理は未実装なので動作しません)
ダイアログを閉じたい場合は、ダイアログの外をクリックしてください。

検索処理を実装する

検索ダイアログで入力した条件で、Google Books APIsを呼び出して結果を表示できるようにしましょう。

まずは、API呼び出しで利用する関数をBookSearchDialog.tsxのインポート文の後ろあたりに作成します。実際のアプリケーションではAPI呼び出し処理は別ファイルにした方がよさそうですが、サンプルなのでBookSearchDialog.tsxに入れることにします。

BookSearchDialog.tsx
function buildSearchUrl(title: string, author: string, maxResults: number): string {
  let url = "https://www.googleapis.com/books/v1/volumes?q=";
  const conditions: string[] = []
  if (title) {
    conditions.push(`intitle:${title}`);
  }
  if (author) {
    conditions.push(`inauthor:${author}`);
  }
  return url + conditions.join('+') + `&maxResults=${maxResults}`;
}

function extractBooks(json: any): BookDescription[] {
  const items: any[] = json.items;
  return items.map((item: any) => {
    const volumeInfo: any = item.volumeInfo;
    return {
      title: volumeInfo.title,
      authors: volumeInfo.authors ? volumeInfo.authors.join(', ') : "",
      thumbnail: volumeInfo.imageLinks ? volumeInfo.imageLinks.smallThumbnail : "",
    }
  });
}

buildSearchUrlはAPIのURLを組み立てる関数、extractBooksはAPIの呼び出し結果(JSON)からコンポーネントが欲しい形でデータを抽出する関数です。上記をコピー&ペーストしてください。

次にAPI呼び出しを実装していきます。
タイミングとしては検索ボタンクリックの際なので、以下のイベントハンドラの「検索実行」の箇所に書けばよいでしょうか?

BookSearchDialog.tsx
  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    // 検索実行
  };

React HooksにおいてはサーバとのAPI通信やLocalStorageへのアクセス等の、コンポーネント内に閉じない処理は副作用(あるいは作用)と呼ばれます。
副作用を実装する仕組みとして、useEffectフックが提供されていますので、その方法をサンプルを通して確認していきましょう。

イベントハンドラ内ではAPIによる検索処理を行わず、モードを変更するのみにします。

BookSearchDialog.tsx
  const handleSearchClick = () => {
    if (!title && !author) {
      alert("条件を入力してください");
      return;
    }
    setIsSearching(true);
  };

isSearchingは現在(のレンダリング処理時点で)検索処理実行中であることを表すboolean型のステート変数で、もちろんuseState関数を使って定義します。

BookSearchDialog.tsx
  const [isSearching, setIsSearching] = useState(false);

useEffectをインポートしましょう。

BookSearchDialog.tsx
import React, { useState, useEffect } from "react";

useStateを使ったステート変数定義の後ろに、useEffectを使った副作用の実装を記述します。

BookSearchDialog.tsx
  useEffect(() => {
    if (isSearching) {
      const url = buildSearchUrl(title, author, props.maxResults);
      fetch(url)
        .then((res) => {
          return res.json();
        })
        .then((json) => {
          return extractBooks(json);
        })
        .then((books) => {
          setBooks(books);
        })
        .catch((err) => {
          console.error(err);
        });
    }
    setIsSearching(false);
  }, [isSearching]);

useEffectの第1引数には、副作用を記述した関数を渡します。
ここでは、isSearchingtrueの場合に以下の副作用を実行します。(isSearchingfalseの場合にもこの関数が呼び出されるので、条件判断は必須です)

  • fetch関数によるAPIコール(Ajax)
  • 結果のJSONから書籍のデータを抽出
  • setBooks関数によるステート変数booksの更新

useEffectの第2引数には、副作用が依存するステート変数およびpropsのプロパティを列挙した配列を渡します。
Reactは、この配列に含まれるステート変数またはプロパティのいずれかの変更を検知した場合にのみ、副作用の関数呼び出しを行います。(省略した場合は、レンダリングの都度毎回呼び出されることになります)

今回の副作用のコードは、検索ボタンがクリックした時に実行されればよいので、isSearchingのみを配列に入れています。(title author props.maxResultsも参照してるぞとeslintに怒られますが、無視してください。気になる場合はこれらを追加しても動作に影響はないはずです)

これで、APIを使って検索を行い結果表示もできるようになったはずなので、動作確認をしてみてください。

書籍を選んでメイン画面のリストへ追加する

書籍の追加(「+」ボタンクリック)時のイベントはBookSearchItem=>BookSearchDialog=>Appと順に伝搬していくように実装済みなので、Appコンポーネントにイベントハンドラを実装します。

その前にAppコンポーネントに置いてあったダミーの書籍情報は不要になったので削除し(const dummyBooks ...を消す)、ステート変数の初期値も空配列にしておきましょう。

App.tsx
  const [books, setBooks] = useState([] as BookToRead[]);

イベント引数でBookDescriptionを受け取るのでインポート文を追加します。

App.tsx
import {BookDescription} from "./BookDescription";

イベントハンドラとなるコールバック関数は以下のようになります。

App.tsx
  const handleBookAdd = (book: BookDescription) => {
    const newBook: BookToRead = { ...book, id: books.length + 1, memo: "" };
    const newBooks = [...books, newBook];
    setBooks(newBooks);
    setModalIsOpen(false);
  }

const newBooks = [...books, newBook]で現在の書籍リストの末尾に、検索ダイアログで選択した書籍情報(から作ったBookToReadオブジェクト)を追加し、setBooks関数でステート変数を更新します。
また、追加後はモーダルダイアログを閉じるために、同様にsetModalIsOpen関数でステート変数を更新します。

JSXを修正し、イベントとイベントハンドラを紐付けます。

<BookSearchDialog maxResults={20} onBookAdd={(b) => handleBookAdd(b)} />

長くなりましたが、これでステップ3は終了です。この時点でのコードはこのようになっているはずです。

ステップ4:書籍を検索して追加する

このステップの内容は、ステップ3終了時の状態から続けて実装します。

ここまででアプリケーションの動作としてはほぼ完成していますが、ブラウザをリロードしても書籍リストが消えてしまわないように、LocalStorageにデータを保存するように実装しましょう。

LocalStorageへのアクセスキーを定数定義します。

App.tsx
const APP_KEY = "react-hooks-tutorial"

const App = () => {

既に述べたように、LocalStorageの読み書きようなコンポーネント内で閉じない処理は副作用としてuseEffect関数を用いて実装するのでした。
useEffectのインポートを追加しておきましょう。

import React, { useState, useEffect } from "react";

まずは書き込み処理です。useStateによるステート変数取得処理の後ろあたりに以下のコードを記述します。

App.tsx
  useEffect(() => {
    localStorage.setItem(APP_KEY, JSON.stringify(books));
  }, [books]);

useEffectの第1引数に渡す関数には、books配列を文字列化した値をLocalStorageに書き込む処理を記述します。
第2引数にはbooks配列を指定することで、booksの内容が更新される都度、この副作用関数が実行されるようになります。

次は読み込み処理です。(注意)先ほどのuseEffectよりも前に記述してください

App.tsx
  useEffect(() => {
    const storedBooks = localStorage.getItem(APP_KEY);
    if (storedBooks) {
      setBooks(JSON.parse(storedBooks));
    }
  }, []);

この副作用は初回のレンダリング時に一度だけ実行すればよいので、このようなケースでは第2引数に空配列[]をしてください。

書籍の追加やメモの更新を行った後、ブラウザをリロードして書籍リストが保持されていることを確認してください。

これでチュートリアルのすべてのステップが完了しました。お疲れさまでした!
最終結果はこちらです。

付録

付録A ソースコード一式

GitHubに上げています。

付録B ローカル開発環境のセットアップ

スターター用のプロジェクトをGitHubに用意しましたので使ってください。
前提条件:以下がインストール済みであること

  • node
  • npm
  • (yarn)
  • git
$ git clone https://github.com/yonetty/react-hooks-tutorial-starter.git
$ cd react-hooks-tutorial-starter
$ yarn install
$ yarn start

もちろんnpmでやってもらっても構いません。(npm install npm start に読み替えてください)

ブラウザでスターターの画面が表示されればOKです。

react_04.png

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

webpackでReact+Typescriptの環境構築をする

Qiita初投稿です。これからどんどん書いていこうとおもうのでご意見・ご要望ありましたら気軽にコメントしていただけると、自身の成長にもつながるのでよろしくお願いします!

webpackとは?

一言で表すとJavascriptを1つのファイルにまとめる事ができるツールです。
他にも様々な機能がありますが、ここでは割愛します。

1つのファイルにまとめるメリット

1つにまとめることでブラウザとサーバーの通信回数を減り、通信速度が速くなります。

実際にやってみる

実際にwebpackを使って、React+Typescriptの環境構築をしてみましょう。

ディレクトリの作成

まず、ターミナルで以下のコマンドでディレクトリを作り、VSCodeで開きます(ファイル名の部分は好きな名前で大丈夫です)。codeコマンドがない場合は普通にファイル作って、VSCodeで開けば大丈夫です。

ターミナル
mkdir [ファイル名]

code [ファイル名]

何もファイルがない状態だと思います。

必要なパッケージのインストール

次にVSCodeのターミナルで以下のコマンドを打つと、package.jsonが作成されます。

VSCodeのターミナル
//npmを使う場合
npm init -y

//yarnを使う場合
yarn init -y

その後必要なパッケージをダウンロードしていきます。

VSCodeのターミナル
//npm
npm install --save react react-dom

npm install --save-dev @types/react-dom @types/webpack @types/webpack-dev-server ts-loader ts-node typescript webpack webpack-cli webpack-dev-server

//yarn
yarn add react react-dom

yarn add -D @types/react-dom @types/webpack @types/webpack-dev-server ts-loader ts-node typescript webpack webpack-cli webpack-dev-server

インストールしたパッケージの簡単な説明を表にまとめました。

react,react-dom Reactを書くのに必要
@types/~ @typesに続くパッケージの型宣言ファイルが含まれている
ts-loader TypescriptをJavascriptにコンパイルするのに使う
ts-node Typescriptのファイルを直接実行できる
typescript Typescriptを書くのに必要
webpack Javascriptのファイルを1つにまとめる
webpack-cli webpackコマンドを使うのに必要
webpack-dev-server ファイルを変えた時に差分ビルドをしてくれる

webpackの設定

次に以下のコマンドを実行して、webpackの設定ファイルである、webpack.config.tsを作成します。

VSCodeのターミナル
touch webpack.config.ts

作成されたwebpack.config.tsに以下のように記述します。webpack.config.jsでも大丈夫です。その場合はConfigurationの部分を消します。

設定ファイルは勉強も兼ねて、デフォルトと同じになっているところも書いているため、少し冗長な部分があります。

webpack.config.ts
import path from 'path';
import { Configuration } from 'webpack';

const config: Configuration = {
    context: path.join(__dirname, 'src'),
    entry: './index.tsx',
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js',
        publicPath: '/assets',
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
            },
        ],
    },
    mode: "development",
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.jsx'],
    },
    devtool: "inline-source-map",
    devServer: {
        contentBase: path.join(__dirname, 'static'),
        open: true,
        port: 3000,
    },
};

export default config;

webpack.config.ts(js)の設定ファイルの簡単な説明を以下の表にまとめました。

path ファイルやディレクトリのpathを操作できる。デフォルトでnode_modulesに入っている
Configuration configのtype
__dirname カレントディレクトリを示す
output ファイルの出力設定(path:出力ファイルのディレクトリ名、filename:出力ファイル名、publicPath:バンドルファイルをアップロードする場所)
module rules(test:コンパイルするファイル、use:コンパイルに使うツール)
mode 開発(development)か本番(production)か
resolve extensions:importで省略したい拡張子
devtool デバッグ用のツール(mode:develop)
devServer 開発用のサーバー(contentBase:サーバーの起点とするディレクトリ、open:ブラウザを自動で起動するか、port:ポート番号)

Typescriptの設定

次に以下のコマンドを実行して、Typescriptの設定ファイルである、tsconfig.jsonを作成します。

VSCodeのターミナル
touch tsconfig.json

作成されたtsconfig.jsonに以下のように記述します。

tsconfig.json
{
    "compilerOptions": {
        "sourceMap": true,
        "baseUrl": "./",
        "target": "es5",
        "strict": true,
        "module": "commonJs",
        "jsx": "react",
        "lib": ["ES5", "ES6", "DOM"],
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "isolatedModules": true,
    }
}

tsconfig.jsonの設定の簡単な説明を以下の表にまとめました。

sourceMap ソースマップを見れるようにするか
baseUrl tsconfig.jsonの場所
target どのバージョンでJavascriptを出力するか
strict 型付けのルールを厳しくする
module Typescriptのモジュールをどのバージョンで出力するか
jsx jsxの書式を有効化
lib コンパイルに使用する組み込みライブラリ
allowSyntheticDefaultImports default importを使うか
esModuleInterop import * 以外も使えるようにするか
isolatedModules exportを必須にするか

次に以下のコマンドでsrcディレクトリを作り、その中にindex.tsxを作成します。

VSCodeのターミナル
mkdir src && touch src/index.tsx

作成したsrc/index.tsxに以下のように記述します。

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

ReactDOM.render(<h1>Hello World!</h1>, document.getElementById('app'));

次に以下のコマンドでstaticディレクトリを作成し、その中にindex.htmlを作成します。

VSCodeのターミナル
mkdir static && touch static/index.html

作成したstatic/index.htmlに以下のように記述します。

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
    </head>
    <body>
        <div id="app"></div>
        <script src="/assets/bundle.js"></script>
    </body>
</html>

 実行

これで最低限の環境は整いました。
VSCodeのターミナルで以下のコマンドを実行します。

VSCodeのターミナル
//npm
npx webpack-dev-server

//yarn
yarn webpack-dev-server

するとlocalhost:3000がブラウザで開かれて、Hello World!が表示されると思います。

image.png

ここまで読んでいただきありがとうございます。少しでもwebpackについてイメージできたら嬉しいです。
今後も記事を書いていこうと思うので感想などいただけるとモチベーションにつながります。

参考記事

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