- 投稿日:2020-09-22T22:34:44+09:00
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.yamlservice: 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例であり、あくまで参考程度にしていただけたらと思います。
間違っている部分等があればご指摘よろしくお願いします。
- 投稿日:2020-09-22T16:16:13+09:00
[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では、candlestickとscatterを同一の
XYChart
というオブジェクト上に描画できそうであった他にも多少調べてみたが、料金がかかるものだったり、ドキュメントが不親切でどこまでできるかよくわからないものしか見つけることができなかった。
(GoogleChartは評判が良くなさそうだったので調べてすらいない。)
ちなみに、料金がかかるものは非常に良いものだったので、本当はそっちを使いたかった。準備
- amChartsインストール
Using React / AmCharts公式ドキュメントより$ npm install @amcharts/amcharts4実装
1. まずは Getting Started で基本的なグラフから
公式ドキュメントのCreating_a_chartに書かれている通りにコーディングすれば、すぐに以下のようなグラフを確認できる。
右端の日付が見切れているが、ウィンドウサイズに対してレスポンシブな上に、表示範囲の拡大縮小時の動作がすばらしく美しい。
ChartJS と ApexChart は使用経験がありますが、そのどちらよりも相当洗練されています。
これはかなり期待できる(`・ω・´)2. 次に candlestick チャートを描画
やり方はここに書いてあります。
が、掲載されているものから若干修正したので、一応修正後のソースコードも掲載。ソースコード
ソースコードはここをクリックして確認
src/App.jsimport React, { Component } from 'react'; import AmChartSample from './AmChartSample'; class App extends Component { constructor(props) { super(props) } render() { return ( <> <AmChartSample /> </> ); } } export default App;src/AmChartSample.jsimport 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;
- 上記ソースコードの通りだと以下のように 休日などのデータがない部分に余計な隙間がある。
dateAxis.skipEmptyPeriods = true;
にすると以下のように 投資関係やってる人からしたらこの表示方法がデフォルトのはず。
ちなみに、このオプションは以下の記事で見つけました。
Amchart (three) candle chart参考情報
- amCharts / XYChart のドキュメント
Anatomy of an XY Chart- amCharts / 基本的な candlestick チャートの実装方法
Taming Candlestick Series- amChartのcandlestick利用について、発展的な内容が含まれていそうな記事
New advanced JavaScript Stock Chart features番外編
3. candlestickと一緒にscatterなどを表示させる
当初筆者がやりたかったことはこれです( ・´ー・`)ドヤッ
candlestickだけでなく、LineやScatterが描画されています!
ソースコード
好き勝手いじっているのでわかりにくくなっているかもしれませんが、ご容赦ください(o*。_。)oペコッ
ソースコードはここをクリックして確認
src/AmChartSample.jsimport 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;番外編の参考
- 投稿日:2020-09-22T16:14:46+09:00
初心者がReact学習歴1週間でWebアプリ作成に挑戦してみた
はじめに
Reactの学習兼ねてWebアプリを作成してみました。
タイトル通り、Reactのインプット期間は1週間程です。
(仕事してるので時間でいうと恐らく数日程度です)今回の記事では、インプット学習歴1週間でもなんとか作れた話をお伝えします。
※挑戦する段階で1週間程度のインプットで開発を始めたという話です。
開発期間は余裕で1ヶ月以上かかりました。私についてですが、今年6月からフロントエンドエンジニアを目指して独学中です。
アプリを作り始めたスキルレベルは、主にHTML/CSS/JavaScriptを勉強しておりました。0から作ったことはないので、今回が初めてのアプリ作成になります。
見ていただくとわかる通り比較的シンプルなアプリなのですが、
私にとっては全く簡単ではなく、、
でも試行錯誤しながら何とか作成できました!今回作ったもの
ログイン機能付きの収支管理アプリです。
いわゆる家計簿アプリ的な物です。
毎月の収入と支出をリスト化し月の残高を表示します。操作動画はTwitterにアップしてます。(画質粗くなってしまいましたが)
ポートフォリオ とりあえず完成したぞーーー!
— Kana (@Kana181003) September 21, 2020
Reactで収支管理アプリを作ってみました。
URLはアクセス制限上載せられないですが、こんな感じです。
デザインも色々真似して拘りました。
まだまだ追加したい機能あるから、引き続き頑張ります!#プログラミング初心者 pic.twitter.com/lYV1xL7ScJこのアプリを作った背景
- 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はNoSQLFirebaseの公式の説明や動画などを見て、どのような形でデータが保存されるのかを試行錯誤しながら進めました。
もちろん、公式以外もいっぱい参考にしてます。最終的なデータの形はこうなっています。
最初は収入と支出分けなくてもと思いましたが、後々の計算の為分けて保存してます。
作り始めの時は、データとログインのユーザーをどうやって繋げるんだ...と思う程未知でしたが、
色々調べてログインしたユーザはuidを使ってうまく個人のデータに分けることができました。スタイル
スタイルは今回メインでCSSを使ってます。
独学初めの当初に勉強してましたが、最近はずっとJavaScriptを集中的に学んでいたので、すっかり忘れていました...デザインの知識は皆無なので、前にチュートリアル でやっていた見た目のデザインを真似しました。
あとは色々な見やすい色合いや配置等を調べて参考にしました。
例えばログイン画面ですら、「あれ、普通どんなんだっけ?」という状況でした。何とか調べながらいい感じにできたわけですが、追加機能をつけるたびにスタイルも調整するという手間があったり。
おそらくCSSメインで書かなければもっと楽だったのかなと思うので、material-uiもっと使いこなせるようになろうと思いましたね。参考記事・動画
教材としては、色々な記事やYoutubeを参考にしました。
何より例があるとより分かりやすかったです!
色々読みまくったので全部は取り上げませんが、分かりやすくて印象に残ってる分を抜粋します。
React hooks
React State and Props | Learn React For Beginners Part 4
※↑YOUTUBE DevEdのシリーズは全部最高です
【React.js】子から親コンポーネントへ値を渡す
useContextの使い方
こんなに簡単なの?React useContextってReact router
React.jsでルーティングを実装するためのreact-routerの紹介
ささっと学ぶReact Router v4Firebase
Firebase公式
What is a NoSQL Database? How is Cloud Firestore structured? | Get to know Cloud Firestore #1
ReactHooks + Firebase(Authentication, Firestore)でTodoアプリ作る
【React】 Firebaseを使用して認証機能の実装
Firebase React Authentication Tutorial For Beginners - Private Route With Hooks追加予定機能(実装中含む)
- 月ごとのデータ管理
- 項目の検索機能(月ごと)
- レシートから料金を文字化してinput(これはできればくらい)
作ってみた感想
よく皆さん言うことですが、アウトプットしながらの勉強は本当に効果があります。
私の場合最初にインプットで学んだ内容は、もはや結局使わず仕舞いでした。たかがReact1週間の学習歴でWebアプリの作成に挑戦したのですが、
結局は作りながら学ぶので、インプット期間は最小限で何とかなります。
インプットに時間をかけても意味がないというのを本当に実感しました。最初学んでる時多少分かった気になっていたんですが、、、
実際に作ってみることできちんと理解していないことがわかりました。(私の場合は)
自分の手で実際に作ることで、きちんと深く理解ができたと思います。あと、思い描いた内容がきちんと反映されて動くのは、最高に嬉しかったですね。
エラーと葛藤してた内容とか、実装方法に苦労してた内容とかは、特に感じました。まだ追加したい機能もありますし、一つ一つ時間もかかりますが、
また新たなアプリを作りながら学習していこうと思います!最後に、今回は一つ一つの機能の実装方法について触れていないので、
次回の記事で初心者のための初心者による記事という感じで、解説していこうと思います。
ご興味ある方は是非ご覧くださいませ。
- 投稿日:2020-09-22T15:07:30+09:00
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.js
にuseState
で状態変数(count
)を定め、関数としてはカウンターの減算(decrement()
)と加算(increment()
)が備わっています。それらをプロパティ(counter
)として受け取るのが、このあと定めるカウンター表示のコンポーネントCounterDisplay
です。コード001■
useState
とプロパティを用いたアプリケーションモジュールsrc/App.jsimport 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.jsimport 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.css
とsrc/App.css
)は、基本的なフォントや余白の設定のみです。確かめたい方は、最後に掲げるCodeSandboxの作例(サンプル001)をご覧ください。図001■でき上がったカウンター
アプリケーションのロジックをカスタムフックに切り出す
アプリケーションモジュール
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.jsimport { 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
)の呼び出しにより、カウンターの状態を操作するための参照(count
とdecrement
およびincrement
)が得られます。前掲コード001とプロパティ名を揃えましたので、カウンター表示のコンポーネント(CounterDisplay
)に渡すプロパティやモジュール(src/CounterDisplay.js
)のコードは書き替える必要がありません。src/App.jsimport { 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.jsimport 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■カスタムフックを使ったカウンター
- 投稿日:2020-09-22T12:00:56+09:00
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 jestjest設定ファイルを生成
$ jest --initcommand not found: jest の場合
以下を実行します。
./node_modules/.bin/jest --initEnzymeインストール
yarn add --dev enzyme enzyme-adapter-react-16Enzymeの利用時は一度だけ
Enzyme.configure()
を呼ぶ必要があるため、下記のスクリプトを追加。jest.setup.jsimport Enzyme from "enzyme"; import Adapter from "enzyme-adapter-react-16"; Enzyme.configure({ adapter: new Adapter() });Jestのテスト前に実行されるようにする。
jest.config.jsmodule.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/" ], ...
- 投稿日:2020-09-22T11:27:19+09:00
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で取得します。Qiitaに動画をアップロードできなかったので、動かしている様子は個人ブログへアップしました。
最終的な結果をここで確認することができます:最終結果
チュートリアルを進める前に実際に画面を操作して動作を把握しておくことをお勧めします。ステップ1:書籍のリストを表示する
スターターコードを確認する
ブラウザからスターターコードを開いてください。以下のようにCodeSandboxの画面が表示されるはずです。
保存時に自動でフォークされて自分専用のサンドボックスとなりますので、このあと自由にファイルの追加や編集を行ってください。左側のペインにはサンドボックスの情報やエクスプローラが表示されています。中央のペインでコード編集を行い、右側のペインでリアルタイムに表示を確認することができます。
スターターには以下のファイルを事前に準備済みです。
ファイル 説明 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.tsxconst 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.tsximport React from "react"; import { BookToRead } from "./BookToRead";次に、propsの型を定義します。
BookRow.tsxtype 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
で受け取ったプロパティを用いてレンダリングを行い、子コンポーネントでonChange
やonClick
などのイベントが発生した際はprops
のプロパティを通じて親コンポーネントにイベントを伝搬します。では次に
App.tsx
から今作ったコンポーネントを利用して表示を行いましょう。インポート文を追加します。
App.tsximport BookRow from "./BookRow";ダミーデータを各要素をJSX要素に変換して変数に格納しましょう。今はまだ
BookRow
コンポーネントから発火されるイベントは無視します。App.tsxconst App = () => { const bookRows = dummyBooks.map((b) => { return ( <BookRow book={b} key={b.id} onMemoChange={(id, memo) => {}} onDelete={(id) => {}} /> ); });繰り返し出力するコンポーネントに対しては、
key
属性を付ける必要があることを思い出してください。
次に、コンポーネントの戻り値となるJSX要素内に展開されるように記述します(クラス名がmain
のsection
要素配下)。return ( <div className="App"> <section className="nav"> <h1>読みたい本リスト</h1> <div className="button-like">本を追加</div> </section> <section className="main">{bookRows}</section> </div> );これで、以下のようにリスト表示されるようになったでしょう。
これでステップ1は終了です。この時点でのコードはこのようになっているはずです。
ステップ2:書籍の削除とメモ書きを実装する
このステップの内容は、ステップ1終了時の状態から続けて実装します。
useStateフックによる状態管理
書籍の削除やメモ書きの変更のイベントを拾い、画面に反映させるためには、書籍のリストをコンポーネントのステート変数として管理する必要があります。
従来のReactでは、そのためにはクラスコンポーネントを作成して、this.state
の中に状態を管理する必要がありました。React Hooks
の導入以後は、関数コンポーネントにおいても状態管理の実現が可能となりました。そのために用いるのが
useState
フックです。
実際にコードを書きながら、使い方を確認しましょう。今回のステップでやりたいことは以下です。
dummyBooks
の内容を初期状態とするステート変数を作成する- 書籍削除のイベントを拾って上記ステート変数を更新する
- 同じくメモ書き変更のイベントを拾ってステート変数を更新する
順に実装していきましょう。
書籍のリストを状態管理する
まずは
useState
関数をインポートします。App.tsximport React, { useState } from "react";この
useState
関数を利用して、ステート変数とその更新用関数を取得します。App.tsxconst App = () => { const [books, setBooks] = useState(dummyBooks);
useState
関数の引数には、そのステート変数の初期値を指定します。
上記のコード例だと、初めてApp
関数コンポーネントが呼び出された際(初回のレンダリング時)にbooks
変数に格納されているのはdummyBooks
で定義したダミーの書籍データの配列となります。ステート変数
books
の内容を表示するようにコードを修正します(dummyBooks
->books
)。ブラウザに表示される結果が変わらないことを確認してください。App.tsxconst bookRows = books.map((b) => { return ( <BookRow book={b} key={b.id} onMemoChange={(id, memo) => {}} onDelete={(id) => {}} /> ); });削除イベントのハンドリング
削除イベントのハンドラ関数を定義しましょう。書籍のIDを受け取り、該当する書籍を配列から削除します。
クラスコンポーネントにおけるstate
の更新と同様、ステート変数の配列を直接操作するのではなく、新しい配列を生成して更新用関数に渡す点に注意してください。App.tsxconst [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.tsxconst 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.tsxconst 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これから作成するのは以下のようなダイアログコンポーネントです。
各々の書籍情報をタイルっぽい形で表示するコンポーネントを先に作ります。
BookSearchItemコンポーネント
CodeSandboxのエクスプローラから、新しいファイル
BookSearchItem.tsx
を作成してください。まずはインポートとpropsの型定義。
BookSearchItem.tsximport React from "react"; import { BookDescription } from "./BookDescription"; type BookSearchItemProps = { description: BookDescription; onBookAdd: (book: BookDescription) => void; };
BookDescription
はAPIで取得した書籍情報のうち、タイトル、著者(群)、サムネイル画像(のURL)を保持する型です。それに加えて、サムネイル画像の右下にある「+」をクリックした際のイベントを拾うコールバック関数を含めたものが当コンポーネントのprops
となります。続いてコンポーネント本体となる関数を定義し、エクスポートします。
BookSearchItem.tsxconst 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.tsximport React, { useState } from "react"; import { BookDescription } from "./BookDescription"; import BookSearchItem from "./BookSearchItem"; type BookSearchDialogProps = { maxResults: number; onBookAdd: (book: BookDescription) => void; };コンポーネント本体の関数を実装していきましょう。まずは
useState
関数を使ってステート変数を定義します。BookSearchDialog.tsxconst BookSearchDialog = (props: BookSearchDialogProps) => { const [books, setBooks] = useState([] as BookDescription[]); const [title, setTitle] = useState(""); const [author, setAuthor] = useState("");
books
は書籍の検索結果を表す配列。初期値は空の配列です。
title
author
は検索条件のタイトルおよび著者名。どちらも初期値は空文字列です。次にイベントハンドラのコールバック関数。
タイトル、著者名のinput
要素のonChange
イベントを拾い、それぞれのステート変数を更新します。BookSearchDialog.tsxconst 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.tsxconst handleSearchClick = () => { if (!title && !author) { alert("条件を入力してください"); return; } // 検索実行 };書籍追加イベントに対するコールバックも実装しておきます。これは子の
BookSearchItem
で発火したイベントを親コンポーネントへ伝搬するだけです。BookSearchDialog.tsxconst 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.tsxexport 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.tsximport 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.tsxconst App = () => { const [books, setBooks] = useState(dummyBooks); const [modalIsOpen, setModalIsOpen] = useState(false);「モーダルダイアログが開いているかどうか」という画面モードをステート変数として持たせて切り替えを行います。最初は閉じていてほしいので、初期値を
false
にしています。イベントハンドラを定義しましょう。
App.tsxconst handleAddClick = () => { setModalIsOpen(true); }; const handleModalClose = () => { setModalIsOpen(false); };
handleAddClick
は「本を追加」ボタンのクリックに対するものなので、モーダルダイアログを開くためにsetModalIsOpen
にtrue
を指定します。
handleModalClose
はモーダルダイアログが開かれた状態で、ダイアログの領域外をクリックした際に呼び出されるものです。(false
を指定します)JSXにコードを追加します。
App.tsxreturn ( <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つです。
- 「本を追加」の
div
にonClick
属性を付与し、handleAddClick
コールバック関数を紐付けModal
コンポーネントを配置し、その子コンポーネントとして先ほど作成したBookSearchDialog
コンポーネントを指定これで、検索ダイアログがモーダル表示できるようになったはずです。(検索処理は未実装なので動作しません)
ダイアログを閉じたい場合は、ダイアログの外をクリックしてください。検索処理を実装する
検索ダイアログで入力した条件で、Google Books APIsを呼び出して結果を表示できるようにしましょう。
まずは、API呼び出しで利用する関数を
BookSearchDialog.tsx
のインポート文の後ろあたりに作成します。実際のアプリケーションではAPI呼び出し処理は別ファイルにした方がよさそうですが、サンプルなのでBookSearchDialog.tsx
に入れることにします。BookSearchDialog.tsxfunction 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.tsxconst handleSearchClick = () => { if (!title && !author) { alert("条件を入力してください"); return; } // 検索実行 };React HooksにおいてはサーバとのAPI通信やLocalStorageへのアクセス等の、コンポーネント内に閉じない処理は副作用(あるいは作用)と呼ばれます。
副作用を実装する仕組みとして、useEffect
フックが提供されていますので、その方法をサンプルを通して確認していきましょう。イベントハンドラ内ではAPIによる検索処理を行わず、モードを変更するのみにします。
BookSearchDialog.tsxconst handleSearchClick = () => { if (!title && !author) { alert("条件を入力してください"); return; } setIsSearching(true); };
isSearching
は現在(のレンダリング処理時点で)検索処理実行中であることを表すboolean
型のステート変数で、もちろんuseState
関数を使って定義します。BookSearchDialog.tsxconst [isSearching, setIsSearching] = useState(false);
useEffect
をインポートしましょう。BookSearchDialog.tsximport React, { useState, useEffect } from "react";
useState
を使ったステート変数定義の後ろに、useEffect
を使った副作用の実装を記述します。BookSearchDialog.tsxuseEffect(() => { 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引数には、副作用を記述した関数を渡します。
ここでは、isSearching
がtrue
の場合に以下の副作用を実行します。(isSearching
がfalse
の場合にもこの関数が呼び出されるので、条件判断は必須です)
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.tsxconst [books, setBooks] = useState([] as BookToRead[]);イベント引数で
BookDescription
を受け取るのでインポート文を追加します。App.tsximport {BookDescription} from "./BookDescription";イベントハンドラとなるコールバック関数は以下のようになります。
App.tsxconst 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.tsxconst APP_KEY = "react-hooks-tutorial" const App = () => {既に述べたように、LocalStorageの読み書きようなコンポーネント内で閉じない処理は副作用として
useEffect
関数を用いて実装するのでした。
useEffect
のインポートを追加しておきましょう。import React, { useState, useEffect } from "react";まずは書き込み処理です。
useState
によるステート変数取得処理の後ろあたりに以下のコードを記述します。App.tsxuseEffect(() => { localStorage.setItem(APP_KEY, JSON.stringify(books)); }, [books]);
useEffect
の第1引数に渡す関数には、books
配列を文字列化した値をLocalStorageに書き込む処理を記述します。
第2引数にはbooks
配列を指定することで、books
の内容が更新される都度、この副作用関数が実行されるようになります。次は読み込み処理です。(注意)先ほどのuseEffectよりも前に記述してください
App.tsxuseEffect(() => { 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です。
- 投稿日:2020-09-22T00:46:08+09:00
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.tsimport 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!が表示されると思います。ここまで読んでいただきありがとうございます。少しでもwebpackについてイメージできたら嬉しいです。
今後も記事を書いていこうと思うので感想などいただけるとモチベーションにつながります。参考記事