20201216のReactに関する記事は11件です。

【React入門】学習メモ #2

この記事はReact Advent Calendar 2020 17日目の記事です。

前回の記事はこちら

はじめに

React学習の備忘録です。
間違い等ございましたら、ご指摘いただけますと幸いです

ブラウザ表示の流れ

ファイルは下記の順番に変換されます。
App.jsに記述されているJSXは最終的にHTMLに変換されてブラウザに表示されます。

  1. App.js
  2. index.js
  3. index.html
  4. ブラウザ(chrome, firefox..)

コンポーネント

コンポーネントとは、「部品」「パーツ」といった意味で、
Reactでは、コンポーネントを組み合わせてUIを構築します。

コンポーネントの定義方法

  • クラスコンポーネント(class)
  • 関数コンポーネント(function)

クラスコンポーネントの構成

Sample.jsx
import React from 'react';  //reactをimport

class Sample extends React.Component {  //React.Componentを継承
  render() {
    return(
      {/* JSX */}
    );
  }
}

関数コンポーネントの構成

Sample.jsx
import React from 'react';  //reactをimport

const Sample = (props) => {
  return(
    {/* JSX */}    
  )
};

コンポーネントがブラウザに表示されるまで

コンポーネントをApp.jsで呼び出して、表示させることで最終的にブラウザに表示されます。
流れは以下です。

  1. Sample.jsx
  2. App.js
  3. index.js
  4. index.html
  5. ブラウザ(chrome, firefox..)

コンポーネントを表示

コンポーネントをApp.jsで呼び出すためには、コンポーネントをexportする必要があります。
下記はクラスコンポーネントの例です。

Sample.jsx
import React from 'react';
class Sample extends React.Component {
  render() {
    return(
      {/* JSX */}
    );
  }
}
export default Sample;  //Sampleコンポーネントをexportする

App.jsでは、
1. 呼び出すコンポーネントをimportして読み込む
2. JSX内でコンポーネントを記述する

App.js
import React from 'react';
import Sample from './Sample';  //【1】Sampleコンポーネントをimportする
class App extends React.Component {
  render() {
    return(
      <Sample />  {/* 【2】Sampleコンポーネントを読み込む */}
    );
  }
}

props

propsとは、コンポーネントから渡される引数的なものです。

以下、引用

  • immutable data(不変のデータ)
  • passed in from parent(親から渡される)
  • can't change it(変更不可)
  • can be defaulted & validated (デフォルト値の設定と検証が可能)

propsの渡し方

「props(プロパティ)名 = 値」という形式で渡す

App.js
render() {
  return(
    <Human
      name = 'masa'  {/* プロパティ: name, 値: 'masa' */}
      age = 21  {/* プロパティ: age, 値: '21' */}
    />
  );
}

propsの取得

this.propsで取得できます。

Human.jsx
render() {
  return(
    <div>
      <div className="human-name">
        { this.props.name }  {/* nameプロパティを取得 */}
      </div>
      <div className="human-age">
        { this.props.age }  {/* ageプロパティを取得 */}
      </div>
    </div>
  );
}

mapメソッドの使い方

コンポーネントの数が多くなるとその分、コンポーネントもpropsも記述しないといけなくなり、コードが肥大化してしまします。そのようなケースにmapメソッドを使用します。

mapメソッドを使用することによって、以下のメリットがあります。

  • 冗長なコードを記述せずに済む(肥大化防止)
  • propsのプロパティと値を配列にまとめることで管理が楽になる
  • コンポーネントを何個も記述せずに済む
App.js
class App extends React.Component {
  render() {
    // 配列
    const humanList = [
      { name: 'masa', age: 21 },
      { name: 'tomoaki', age: 24 },
      { name: 'naoki', age: 15 },
      { name: 'takahiro', age: 51 },
      { name: 'haru', age: 37 }
    ]
    return(
      <div>
        {humanList.map((human) => {
          return(
            <Human
              name = { human.name }
              age = { human.age }
            />
          )
        })}
      </div>
    );
  }
}

mapメソッドでは、配列の格propsがhumanList.mapの引数(human)に格納されてます。
それからコンポーネント側で、{ human.name }や{ human.age }でpropsを渡しています。

備考

良ければ、続きの記事も見て頂けますと幸いです。

【React入門】学習メモ #3

参考記事

Reactにおけるstateとpropsの違い

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

React備忘録

ReactHooksについて

・useState
・useReducer
・useContext
・action.js
・useEffect
・combineReducers(Redux)
適宜追加していきます。

useState

const [state, setState] = useState(initialState);

のような形で、左側は自分で命名したstate、右側はsetの次は大文字にする。右側のset~を使ってstateの値を更新する。useStateの()には初期値を設定する。

useReducer

useReducerを用いることで、ステート更新関数をステート非依存にすることを強制できます。実際のアプリ開発においては、アプリが複雑化するにつれて、あるステートと別のステートが関わりを持ち始めるかもしれません。

App.jsx
 const initialState = {
    events: [],
    operationLogs: []
  };
  const [state, dispatch] = useReducer(reducer, initialState);
reducers/event.js
import { CREATE_EVENT, DELETE_ALL_EVENTS, DELETE_EVENT } from "../actions";
const events = (state = [], action) => {
  switch (action.type) {
    case CREATE_EVENT:
      const event = { title: action.title, body: action.body };
      const length = state.length;
      const id = length === 0 ? 1 : state[length - 1].id + 1;
      return [...state, { id, ...event }];
    case DELETE_EVENT:
      return state.filter((event) => event.id !== action.id);
    case DELETE_ALL_EVENTS:
      return [];
    default:
      return state;
  }
};
export default events;
components/Event.jsx
const addEvent = (e) => {
    e.preventDefault();
    dispatch({
      type: CREATE_EVENT,
      title,
      body
    });
    dispatch({
      type: ADD_OPERATION_LOG,
      description: "イベントを作成しました",
      operatedAt: timeCurrentIso8601
    });
    setTitle("");
    setBody("");
  };

コードの説明

App,jsx
const [state, dispatch] = useReducer(reducer, initialState);
を宣言します。そうすることで現在の statedispatchメソッドとペアにして返します。
components/Event.jsxのfunction内でdispatch呼ぶことでtypeとパラメーターをreduers/event.jsにactionとして渡します。
こうして渡ってきたactionをreduers/event.jsないでcase文で分岐させます。
現在stateにeventsというオブジェクトがあり、その中にデータとして配列で格納されています。

useContext

useContextを使うメリットはpropsによるバケツリレーをしなくて済むので、不要なコードを書かなくてよくなる。

contexts/AppContext.js
import { createContext } from "react";
export const AppContext = createContext();
App.jsx
return (
    <>
      <AppContext.Provider value={{ state, dispatch }}>
        <EventForm />
        <Events />
        <OperationLogs />
      </AppContext.Provider>
    </>
  );
components/Events.jsx
import { AppContext } from "../contexts/AppContext";
export const Events = () => {
  const { state } = useContext(AppContext);

コードの説明

contexts/AppContext.jsでcreateContext();を宣言します。
そして、App.jsxAppContext.Provider value={{ state, dispatch }}という形で
以前はcomponentsごとに書いていたpropsを一括してAppContextとして記述します。
そうすることで、components内でstateだけを呼びたいのであれば
const { state } = useContext(AppContext);と記述して
両方呼びたいのであれば
const { state, dispatch } = useContext(AppContext);と記述する。
useContextを使うことでcomponents内でpropsとして渡さなくてもよくなる。

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

ReactでAirtableのAPPS(アプリ)を作ろう!!

みなさんこんにちは。
NTTテクノクロスの上原です。業務ではGatsbyを使って内製キュレーションサイトの構築や運用などを行なっています(関連で一昨年書いた記事→Gatsbyの真の力をお見せします)。
これは、NTTテクノクロスアドベンドカレンダー2020 の20日目の記事です。昨日の記事は @y-ohnuki さんによる「iPhone12ProのLiDARスキャナを試してみる」でした。

去年はこんな記事(React-SpringのHooks APIでブラウザアニメーションを基本から極めよう!)を書いてたわけですが、今年は新型コロナでいろいろとたいへんだった年でした。みなさまは、いかがお過しだったでしょうか。一年があっという間ですね。来年は何をしているのだろうか。

今年はノーコード・ローコード開発サービスであるAirtableのAPPS機能をプログラマ目線で紹介します。

対象読者

  • ノーコード開発ツールに興味がある技術者
  • マイクロフロントエンド的にReactアプリを結合させて機能させることに興味がある人
  • 未来のアプリ開発のやりかたについてインスピレーションを得たい人

Airtableって何?

Airtableはブラウザから利用できるスプレッドシートのインターフェースを持ったオンラインデーターベースサービスです。カレンダーやカンバン的に表示したり、スケジュール管理に使ったり、ワークフローを組み合わせたりもでき、スプレッドシートの自然な拡張として、専用の業務アプリが提供するような機能を、非プログラマでも直感的かつ柔軟に達成できるサービスであり、アプリ開発環境でもあります。

Airtableは2013年からの老舗でありますが、最近流行りの「ノーコード開発プラットフォーム」の代表とも言え、2020年9月にも$185Mという大規模な資金調達を成功させ、評価額25億8,500万米ドルを誇るユニコーン企業としても注目を集めています。

Airableはもともとはコードを一切書かない・書けない「ノー」コード開発ツールでしたが、2020年9月にリリースされたIFTTTライクなAutomation、JavaScript/TypeScriptによる機能拡張を可能にするAPPS(およびScripting APPS)の提供によってローコード開発ツールに展開されたことになります。

ビジネスとして、またツールの有用性としてAirtableの成功は興味深いものですが、「未来のWebアプリ開発」を見通すことに関してソフトウェアデザイン、エコシステムとしても非常に興味深いものです。本記事ではプログラマ目線でノーコード開発ツールの一つとして紹介し、印象をお伝えしたいと思います。

Airableサービスはどんな感じのものか?

Airableサービスにサインアップしてログインすると
1.png

こんな感じの画面。並んでいるアイコンは「ベース」と呼ばれる単位で、Excelでいうxlsファイル(ブック)に相当します。ベースをクリックすると以下のような、タブで切り替えられる「テーブル」の集合を見ることができます。

ベース:
2.png
ブラウザ上で、まずはOffice 365のWeb版ExcelやGoogle Spread Sheetのように直感的に使うことができます。ベースは概念的に以下のような構造をもっています。
3.png
ベースはテーブルの集合で、Excelでいえばシートをあつめたものがブックになる、というイメージです。ただしAirtableでは常に「ビュー」というレイヤを1層挟んでテーブルをユーザが見たり操作したりすることになります。

今までに出てきた用語を表にまとめると以下のとおりです。

Airtableの用語 Excelでの対応物 意味
ワークスペース ブックが格納されているフォルダ 共同作業者(コラボレータ)と共有できる単位のベースの集合。プロジェクト。
ベース ブック(.xlsx) テーブルの集合。1画面。テープルをタプで切り替えられる。
テーブル ワークシート レコードの集合
レコード ワークシートの1行 テーブルの1行
ビュー 対応物なし テーブルの見せかた。不要なものを隠蔽したりソートしたり
フィルタしたり。kanbanやカレンダーの
ビューなどに対応。
APPS 対応物なし(あえて言え
ばグラフ領域やピボット
テーブル領域)
ミニアプリ。

Airtableの特徴はなんなの?

以下が特徴になります。

  1. データモデリングを出発点かつ中心としたツール。
    • レコードの集合としてのテーブルを定義していく。レコードのフィールドは静的な型を持っている。
    • SQLが不要なリレーショナルデータベースでもある。 リレーションやカーディナリティ(1:n、m:n…)を設定できる。
    • テーブル間の関係をExcelのセル操作のようなレベルでとても直感的に設定できる。
  2. Excelライクなわかりやすいユーザインターフェースで扱う
    • 機能の大半はExcelのような汎用インターフェースで達成する。一般アプリとまったく同じ使い勝手のUIはめざさない。
  3. その他
    • 実データを活用したRest APIと参照ドキュメントがリアルタイムに生成される。実際のデータを元にしてサンプル出力などを表示するのでわかりやすい。
    • APPS(後述)で拡張可能。一例としてGraphQL APIも生成できる(BaseQL APP)。

「ノーコード開発ツール」といってもいろいろですが、Airtableの方向性は明確です。それは、現実社会で一般に、業務の多くがExcelのような表計算でまかなえていることに着目し、その延長・拡張として機能実現することです。考えてみましょう、一般の事務職や会社員が、いかにExcelとファイル共有という汎用インターフェースだけで多くの業務をこなしてきているかを。このコンセプトによってAirtableは圧倒的なとっつきの良さ、開発速度と機能カバーを実現しています。

なお、本記事で言及しているのはAirtableのごく一部です。Gatsbyのデータソースに使えるなど、連携機能も豊富です。

ノーコード開発ツールについて少しだけ

字面から言えば、「ソースコード」を一切書き下さずにソフトウェアを開発するものがノーコード開発ツールであり、コードを書く量が少ないものの、ゼロではないものがローコード開発ツールです。とはいえ境界はあいまいです。

現代的な意味のノーコード・ローコード開発ツールは、一般にクラウドサービスとして提供されていることが必要です。いわゆる「ビジュアルプログラミング言語」であればノーコード開発ツールと言えるかというと多分違うのであって、データ管理や実行環境を含めてクラウドサービスとして機能を利用できることが従来からのツールとの違いであり、わざわざ「ノーコード・ローコード」と新しい名前で呼ぶ理由の一つであると言えます(例外はたぶんありますが)。

つまり、ソフトウェアライフサイクルのうちのコーディングだけではなく、ビルド、デプロイ、テスト支援といった開発支援系機能を統合サービスとしてクラウド上で提供するものです。

とはいえ、従来からあったもののリブランデイングで呼び名が新しいだけ、という面もあるのもたぶん確かです。

Airtable APPS(アプリ)

APPSはReactで書く「ミニアプリ」です。

AirtableがExcelだとしたら、Excel中に埋め込める「グラフ領域」や、「ピボットテーブル領域」を想像してみると少し近いです。実際、Airtableのピボットテーブルやグラフ機能はAPPSとして利用できます。なお、APPSは有償のPro Planのみで使用できる機能です(ただ2020年12月現在、登録後2週間はPro Plan無料で利用できるようです)。ただし、自分でビルドして利用するカスタムAPPSは、今のところ無料版(Free Plan)でも開発したり使用することができるようです(ただし保証や将来も利用できるかものかなどは不明)。なお、APPSは以前はBlocksという名前でしたが、名前が変更されました。以降で時々出てくるblockはAPPSと同義です。

APPSの実行の様子は以下のとおりです。

4.png

APPSは「ダッシュボード」の中にまとめて表示でき、利用者はダッシュボードをカスタマイズしてそれを表示することでノーコードプラットフォームとしてのAirableをAPPSを通じて利用することができます。

Appsは個別にAirtableのベースに追加し、Airtableの中で実行して使用します。単独では使用できません。

ReactでAPPSを作ろう!! ???

以上は前置きでした。以降が本題です。早速カスタムAPPSを作ってみましょう。APPSはFirebaseのようにリアルタイムデータ更新を扱えますので、チャットアプリを作ってみます。

完成予想図はこんな感じです:
スクリーンショット 0002-12-16 17.36.00.png

データモデリング

Airtableの開発の流儀としてデータモデリングを最初にやります。ここでは「発言内容」「作成者」などのフィールドを持ったテーブルを定義し、それにチャットアプリをアタッチできるようにします。

準備として、まずベースを新規作成(ベース名「チャット」、アイコンを適当に設定)を作成し、デフォルトで作成されているテーブルを編集し、テーブル名「発言一覧」にして、レコードヘッダの「Customize Field Type」を選択して以下のようなテーブルを作成します。

フィールド名 Type 内容 GUI上での入力内容
ID(Primary Field) Autonumber - f1.png
日時 Formula CREATED_TIME() f2.png
内容 Long text - f3.png
発言者 Created By - f4.png

以上より、以下のような「発言一覧」テーブルが作成されます。

5.png

ここでは使用しませんでしたが、フィールドに「Link to another record」という型を指定することで、他のテーブルのフィールドの値をRDBで言うところの外部キーとして使用することなどができます。

ひながた生成

では自作のAPPSである「カスタムAPPS」を作成していきましょう。以下の操作を行います。

  1. 準備として、画面右上の「Account」メニューから「Account」を選び、表示されるAPIキー(★1)を確認します。
  2. ベースの画面、右側の「APPS」をクリック
  3. 「Install an app」をクリック
  4. 「Build a custom app」をクリック
  5. テンプレートギャラリーから「Hello world (TypeScript)」をチェック
  6. 「App name」に「Chat」を入力、「Creating App」ボタンをクリック

以降、表示されるガイダンスに従います。

  1. ターミナルで「npm install -g @airtable/blocks-cli」を実行
  2. 同じくターミナルでAPPSの雛形の生成を行います。(なお現在のblock initはProxy背後ではエラーになるかも)
block init appO9XXXXXXXXXXXX/blkYYYYYYYYYYYYYY --template=https://github.com/Airtable/apps-hello-world chat
  1. 初回は以下が表示されるので、(★1)で用意していたAPIキーを入力します。
? Please enter your API key. You can generate one at https://airtable.com/account

これでうまくいけばAPPSアプリのソースコード雛形が作成されます。

Using your existing API key from /Users/uehaj/.config/.airtableblocksrc.json
Initializing block using https://github.com/Airtable/apps-hello-world-typescript template
[npm]
[npm] > core-js@3.8.1 postinstall /Users/uehaj/work/lowcodenocode/airtable/air_chat/node_modules/core-js
[npm] > node -e "try{require('./postinstall')}catch(e){}"
[npm]
[npm] added 225 packages from 225 contributors and audited 225 packages in 55.392s
[npm]
[npm] 23 packages are looking for funding
[npm]   run `npm fund` for details
[npm]
[npm] found 0 vulnerabilities
[npm]
✅ Your block is ready! cd air_chat && block run to start developing, and npm run lint to lint.

カスタムAPPSの実行

一旦以下を行いAPPSを実行してみます。

cd chat
block run

うまく行けば以下が表示されます。

6.png

  • ブラウザに戻って、「Continue」をクリックし、
    7.png

  • block runで表示されたURL、さっきの場合だと「https://localhost:9000 」を入力します(ポート番号は異なることがあり)

  • ブラウザでは以下のようにアプリが実行されています。

8.png

ソースを修正して保存すると自動リロードが走ります。

ReactコードとしてのAPPS

APPS SDK(Blocks SDK)が提供するのはReactをベースとしたSDKです。ただし開発できるアプリには以下の制限や特徴があります。

  • Airtable画面の一部の矩形領域パーツ(iframe)として実行される。メインメニューや自身の複雑なレイアウトはもたない。
  • アプリのデプロイとホスティングは気にしなくてよい。ローカルビルドのときはlocalhostでdev serverが動作するが、いったんblock releaseすればAirtableのサーバ内でホスティングされる。
  • webpackの設定など、こまかいことはできない。
  • 任意のnpmモジュールの使用が可能。
  • スタイルシステムはAirtable提供のもの(loadCSSromStringなど)を使用する。CSS in JSなどもやればできると思うが、全体のスタイルと不一致となるし、画面を占有できないし、一工夫が必要だと思われる。(あまり凝るべきではないのかもしれません)

APPSにはUIデザインガイドラインがあり、Airtableの中でよりよく機能を発揮するように、コンポーザブル(合成可能)で、柔軟性があり、協調的に動作する、といった指針が定義されています。

hello world APPSのコード

APPSの開発を始める前に、block buildコマンドでデフォルトとして生成されたHello Worldアプリ(flontend/index.tsx)の中身を見てみましょう。

import {initializeBlock} from '@airtable/blocks/ui';
import React from 'react';

function HelloWorldTypescriptApp() {
    // YOUR CODE GOES HERE
    return <div>Hello world ?</div>;
}

initializeBlock(() => <HelloWorldTypescriptApp />);

Create React Appで生成したReactアプリのようなReactDOM.render()を行なわないことに気付きます。その代わりにinitializeBlock()」にコンポーネントを渡します。それ以外は基本的には普通のRactアプリです。

Chatアプリ開発

ではチャットアプリを作っていきます。フォルダ・ファイル構成はこんな感じです。

chat/frontend
├── components
│   ├── ChatPanel.tsx
│   └── Setup.tsx
├── index.tsx
└── useConfig.tsx

./index.tsx

トップレベルに置くindex.tsxを置き換えて以下の内容のとおりにします。

index.tsx
import {
    Box,
    initializeBlock,
} from '@airtable/blocks/ui';
import React from 'react';
import ChatPanel from './components/ChatPanel';
import Setup from './components/Setup';

function ChatApp() {

    return (
        <Box flexDirection='row' display="flex">
        <Box flex="8" padding={3} ><ChatPanel /></Box>
            <Box flex="auto" ><Setup /></Box>
        </Box>
    );
}

initializeBlock(() => <ChatApp />);

index.tsx解説

ここで使用しているBoxはdivに展開されるコンポーネントです。flexboxの制御のためのパラメータを指定できます。ここらへんのUI部品群はMaterial UIなど既存のものではなく、Airtable独自のもののようです。

./useConfig.ts

アプリの設定情報にアクセスするためのhook。

import { useGlobalConfig } from '@airtable/blocks/ui';

const configKeys = [
  'selectedTableId',
  'selectedViewId',
  'selectedMessageFieldId',
] as const;

type ConfigKeys = typeof configKeys[number];

export default function useConfig() {
  const globalConfig = useGlobalConfig() as {
    get(key: ConfigKeys): string;
  };
  const selectedTableId = globalConfig.get('selectedTableId');
  const selectedViewId = globalConfig.get('selectedViewId');
  const selectedMessageFieldId = globalConfig.get('selectedMessageFieldId');

  return {
    selectedTableId,
    selectedViewId,
    selectedMessageFieldId,
  };
}

useConfig.ts解説

globalConfigはBlock SDKの特徴的な機能の一つで、APPSのインスタンスごとにサーバサイドに確保される設定情報を保存するストレージだと思ってください。キー名のハッシュとして任意の値を保存・取得できるのですが、型つきで扱うために、キーをas constした文字列配列にしています。

ここで保存しているのは、アプリと、テーブルのビュー・カラムに対する紐付け情報です。具体的には、APPSの初回実行時に下図のように「どのテーブル」「どのビュー」「どのフィールド」かなどを処理対象として指定して紐付けます。その対応は上記の処理によって明示的にglobalConfigに保存します。

9.png

ちなみにGlobalConfigが壊れた場合にアプリが起動しなくなるなどがありえます。あるいはフィールドの紐付けをやりなおしたい場合は、その機能を作り込まなくてもAirableのUI(APPSの「Glocal config」)からClearを行うことができます。

./components/Setup.tsx

前述のGlobalConfigを設定するためのUIです。

import React, { useState } from 'react';

import {
  TablePickerSynced,
  ViewPickerSynced,
  FieldPickerSynced,
  FormField,
  Box,
  useBase,
} from '@airtable/blocks/ui';
import useConfig from '../useConfig';

export default function Setup() {
  const { selectedTableId } = useConfig();
  const base = useBase();
  const table = base.getTableByIdIfExists(selectedTableId);

  return (
    <>
      <Box padding={3} borderBottom="thick">
        <FormField label="テーブル">
          <TablePickerSynced globalConfigKey="selectedTableId" />
        </FormField>
        <FormField label="ビュー">
          <ViewPickerSynced table={table} globalConfigKey="selectedViewId" />
        </FormField>
        <FormField label="Created byフィールド">
          <FieldPickerSynced
            table={table}
            globalConfigKey="selectedCreatedByFieldId"
            placeholder="Pick a 'created by' field..."
          />
        </FormField>
        <FormField label="Messageフィールド" marginBottom={0}>
          <FieldPickerSynced
            table={table}
            globalConfigKey="selectedMessageFieldId"
            placeholder="Pick a 'message' field..."
          />
        </FormField>
      </Box>
    </>
  );
}

Setup.tsx表示

以下のように表示されます。

10.png

Setup.tsx解説

Airtable APIには以下のような、存在するテーブル/ビュー/フィールドをそれぞれ選択するための専用のGUI部品があり、それにGlobalConfigのキー名文字列を指定するだけでGloalConfig領域への読み書き含めて行なってくれます。

  • TablePickerSynced
  • ViewPickerSynced
  • FieldPickerSynced

いずれも、存在するテーブルやビュー名から選択するSelectユーザインターフェースで設定できます。

11.png

./components/ChatPanel.tsx

チャットのメイン画面です。

import React, { useState } from 'react';
import {
  useBase,
  useRecords,
  Box,
  Input,
  loadCSSFromString,
} from '@airtable/blocks/ui';
import useConfig from '../useConfig';

loadCSSFromString(`
.base {
  background-color: #34569b;
  padding: 0.5rem;
  border-radius: 10px;
}
.balloon {
  position: relative;
  display: block;
  margin: 0.5rem 100px 1.0rem 10rem;
  padding: 10px 10px 20px 10px;
  min-width: 120px;
  max-width: 100%;
  margin-left: 20px;
  color: #555;
  font-size: 16px;
  background: #e0edff;
  border-radius: 15px;
}

.balloon:before {
  content: "";
  position: absolute;
  top: 50%;
  left: -15px;
  margin-left: -10px;
  margin-top: -15px;
  border: 15px solid transparent;
  border-right: 15px solid #e0edff;
  z-index: 0;
}

.balloon p {
  margin: 0;
  padding: 0;
}
`);

export default function ChatPanel() {
  const {
    selectedTableId,
    selectedViewId,
    selectedCreatedByFieldId,
    selectedMessageFieldId,
  } = useConfig();
  const base = useBase();
  const table = base.getTableByIdIfExists(selectedTableId);
  const view = table ? table.getViewByIdIfExists(selectedViewId) : null;
  const messageField = view
    ? table.getFieldByIdIfExists(selectedMessageFieldId)
    : null;

  const records = useRecords(view, {
    fields: [selectedCreatedByFieldId, selectedMessageFieldId],
  });

  return (
    <Box className="base">
      {messageField && (
        <Box>
          <input
            onKeyPress={(e: any) => {
              if (e.key === 'Enter') {
                table.createRecordsAsync([
                  { fields: { [selectedMessageFieldId]: e.target.value } },
                ]);
                e.target.value = '';
                e.preventDefault();
                return false;
              }
            }}
            placeholder="発言をどうぞ"
            size={50}></input>
          {records &&
            records
              .slice()
              .sort((a, b) => b.createdTime.getTime() - a.createdTime.getTime())
              .map((msg) => (
                <div className="balloon">
                  <p>{msg.createdTime.toLocaleString()}</p>
                  <p>
                    {(msg.getCellValue(selectedCreatedByFieldId) as any).name}
                  </p>
                  <p>{msg.getCellValue(selectedMessageFieldId)}</p>
                </div>
              ))}
        </Box>
      )}
    </Box>
  );
}

ChatPanel.tsx表示

以下のように表示されます。

12.png

ChatPanel.tsx解説

以下、個別に説明します。

loadCSSFromString(`
.base {
  :
});

loadCSSFromString()でクラス名指定のスタイルシートを設定します。CSS in JS的な方法もおそらくは適用できるのでしょうが調べきれず。

  const {
    selectedTableId,
    selectedViewId,
    selectedCreatedByFieldId,
    selectedMessageFieldId,
  } = useConfig();

GlobalConfigからの読み込みを処理を行うカスタムHook、useConfig(前述)を使用して設定項目を取り出します。

  const base = useBase();

useBaseはAirableのベースを取得します。useStateの様に動作し、すなわち変更があったときだけ新しい値でレンダリングが行なわれます。

  const table = base.getTableByIdIfExists(selectedTableId);
  const view = table ? table.getViewByIdIfExists(selectedViewId) : null;
  const messageField = view
    ? table.getFieldByIdIfExists(selectedMessageFieldId)
    : null;

baseから順に、テーブル、ビュー、フィールド名を取得します。

  const records = useRecords(view, {
    fields: [selectedCreatedByFieldId, selectedMessageFieldId],
  });

最終的に表示したい発言のリストを、使用したいフィールドを指定して取得します。

          <input
            onKeyPress={(e: any) => {
              if (e.key === 'Enter') {
                table.createRecordsAsync([
                  { fields: { [selectedMessageFieldId]: e.target.value } },
                ]);
                e.target.value = '';
                e.preventDefault();
                return false;
              }
            }}
            placeholder="発言をどうぞ"
            size={50}></input>

JSXではonKeyPressハンドラを設定した入力フィールドを用意します。Inputではなくinputを使っているのはonKeyPressハンドラが指定できなかったためですが原因不明。

          {records &&
            records
              .slice()
              .sort((a, b) => b.createdTime.getTime() - a.createdTime.getTime())
              .map((msg) => (
                <div className="balloon">
                  <p>{msg.createdTime.toLocaleString()}</p>
                  <p>
                    {(msg.getCellValue(selectedCreatedByFieldId) as any).name}
                  </p>
                  <p>{msg.getCellValue(selectedMessageFieldId)}</p>
                </div>
              ))}

発言を作成日時でソートして表示します。ビュー側でソートすることもでき、ビューモデルとしてはその方が正しいかもしれませんが、ここではクライアント側でソートしておきます。

Chatアプリの実行???

上記のソースコードを保存すると、刻々とAirtableのAPPS領域のダッシュボード内に表示されるアプリが更新されていくと思います。完成したならば、Setupで表示されるフィールドに実際のベースのテーブル、ビュー、発言フィールドを選択させます。先に作成していたテーブルであれば、テーブルに「発言一覧」、ビューに「Grid View」、「Created Byフィールド」に「内容」、Messagesフィールドに「内容」を設定します。

13.png

するとアプリが結びついてチャットができるようになります。

airtable.mov.gif

Chatアプリのリリース?????

動作確認が済んだら以下を実行してアプリをAirbleのサーバサイドにリリースすることができます。

block release

リリースするとblock runをローカル実行する必要がなくなります。一回リリースすると、修正には再リリースが必要になります(localhostに立てたdev serverを使ってのリアルタイム更新はできなくなる)。

なお、カスタムAPPSをマーケットプレースに公開することもできるし(レビューあり)、Gibhubにソースコードを公開しておくことで他者にカスタムAPPSとしてビルドしてもらう前提で公開することもできます。本稿で作成したアプリは以下に公開しております。カスタムAPPSのBuild An Appのときに「Remix from GitHub」を選びURLを入力することで選択できる雛形として利用することができます。(やってるいることはgithubの指定プロジェクトの最新のソース内容を展開し、ベースとの紐付けのIDを含んだ.block/remote.json作成することです)

https://github.com/uehaj/airtable-chat

Airtableでのプログラミング、設計、開発について

Airtableを調査するにあたってプログラマとして気づいたことを列挙してみます。

(1)データモデリングから始めよう

一般に、Airtableでのアプリ開発ではデータモデリングから開始します。Airtableのデータモデリングとは、テーブルのフィールドの「型」を設定していくことです。これは常に実データを見ながらDBテーブルのスキーマ定義を行うことです。場合によってはAPPSを使用せずに業務が完結するかもしれません。APPS開発をするとしたら、アジャイル開発で求められる条件「最初の段階からミニマムな機能が動いていること」が達成できています。

(2) フォーミュラは副作用なしメソッド

Airtabbleのフィールドに設定できる型は多彩ですが、特徴的な型の一つに「フォーミュラ(Formula)」があります。これはExcelのセル式に対応するように見えますが、カラム全体の設定であることが大きな違いです。その違いが何を生んでいるかというと、レコードを「クラス定義」とみなしたとき、フォーミュラがメソッド定義の機能を担えることです。副作用がないのでデバッグは容易です。CQRS(コマンドクエリ責務分離)のクエリとも言えます。

APPSを組み合せるときでも、ビューまわりのための加工はフォーミュラとしてサーバサイドで実行してしまうことができます。SPAと組み合わせる場合、ビューモデルをサーバサイドで作っているということでもあります。

(3) REPLのように

実データをもとに、「動くデータ」を元にして、処理結果を見ながらアプリを開発していきます。

おわりに

クラウドの隆盛と開発技術の進展で、ツールやMbaaSなどを活用して、新規コード開発以外の方法で効率良く機能達成を行なうことが求められている時代を迎えています。プログラミングが授業で教えられる時代であり、アプリ利用とアプリ開発の垣根が下っていくことにも間違いありません。しかし、その先にあるものはプログラマとして見ても、あるいはプログラマ視点で見るからこそ、依然として豊かで興味深いものです。私はAirtaleのAPPSの例を通じて近未来のアプリ開発のありかたの一端を感じました。その一端が伝わればと思います。
ちなみにノーコード・ローコード開発の分野は奥が広く、Outsystems、Mendix、ほかさまざまな別の方向でそれぞれに別世界が開かれています。

明日は@nakasho-devさんの記事です。

では良いお年を!

参考

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

React チュートリアル

はじめに

Reactチュートリアルの追加課題の解答と、私が解いていく中で詰まったところを記述する。ReactやJSに関する知識が拙い中解いたので、より良い解き方などがあると考えられるが、一例として参考にしていただきたい。(より良い解き方など指摘いただければ追記致します。)
コードはチュートリアル内の最終結果を改変した。

1.履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。

考え方

着手の位置を履歴として残すために、historyに新たな要素として追加する。その後movesに着手の位置も表示するように変更すればよい。

解答例

まず、Gameクラスのコンストラクタにあるhistoryと盤面が動いた際の処理が書かれたメソッドhandleClick内のhistoryputStateをkeyとした着手の位置を保存するための要素を追加する(history自体ではなくhistory内にある辞書やhistoryに追加する辞書に対しての意)。
コンストラクタでは以下のように変更する(ここで定義しなくても動く)。

history: [
    {
        squares: Array(9).fill(null),
        putState: "(0, 0)"
    }
]

だが、handleClickメソッドでは着手の位置を与える必要がある。幸運なことに(当然だが)このメソッドは選択したマスの番号を引数として与えられているので、以下のように変更するだけでよい。

history: history.concat([
    {
        squares: squares,
        putState: ` (${i/3|0}, ${i%3})`
    }
])

JavaScriptのバージョンによっては文字列の埋め込みがこのようにできないので以下のようにする。

history: history.concat([
    {
        squares: squares,
        putState: " (" + (i/3|0).toString() + ", " + (i%3).toString() + ")"
    }
])

このようにして着手の位置を履歴として残せるようになったので最後にmovesを変更し、着手の位置を表示させる。

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

2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。

考え方

選択されている盤面はGameクラスのメンバ変数StepNumberに格納されている。また、着手履歴の表示に関する部分はmovesに格納されている。このmovesは着手履歴を保存しているhistoryを用いてrenderされる度に生成される。さらに表示したい盤面を着手履歴のリストから選択するたびStepNumberが更新され再度renderされる。つまり、選択のたびrenderされるのでmovesが生成されるときにStepNumberと一致する番目にあるアイテムをボールドするようにすれば良いと考えられる。

解答例

変更点はmovesの定義部分のみ

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

最初falseの時のdesc{desc}と書いていてerror({}が二重になるため)を出していたのでこの部分は注意したい。

3.Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。

考え方

単純にリストを作って二重ループ回してリストにJSXを適宜格納し、ハードコーディング部分をリストに置き換える。

解答例

考え方通りコードを書く。

render() {
    const squareBoard = [];
    const row = 3;
    const col = 3;
    for(let i=0; i<row; i++){
        let rowBoard = [];
        for(let j=0;j<col; j++){
            rowBoard.push(this.renderSquare(j+3*i));
        }
        squareBoard.push(
            <div className="board-row">
                {rowBoard}
            </div>
        )
    }
    return (
        <div>
            {squareBoard}
        </div>
    );
}

これで上手くいくと考えていたが警告が出る。
Warning: Each child in a list should have a unique "key" prop.
これはkeyに関する警告でチュートリアル中にも触れられていたものである。keyを設定することでVirtualDOMのdiffから実際のDOMに反映させるときの変更を最小限にすることができる。変更を施すと以下のようになる。

class Board extends React.Component {
    renderSquare(i) {
        return (
            <Square
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
                key={"square-"+i.toString()}
            />
        );
    }

    render() {
        const squareBoard = [];
        const row = 3;
        const col = 3;
        for(let i=0; i<row; i++){
            let rowBoard = [];
            for(let j=0;j<col; j++){
                rowBoard.push(this.renderSquare(j+3*i));
            }
            squareBoard.push(
                <div className="board-row" key={"row-"+i.toString()}>
                    {rowBoard}
                </div>
            )
        }
        return (
            <div>
                {squareBoard}
            </div>
        );
    }
}

ここでは"row-"+i.toString()としているがiだけでも問題ない(Squareも同様)。

4.着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。

私はトグルボタンそのものを実装することは本質ではなく、コードが複雑になると考えたので単純にボタンで実装した(トグルボタンにしたい方はトグルボタンのコンポーネントを作成してボタンのタグをトグルボタンのタグに変更することでできる)。

考え方

着手履歴は、Gameクラスのメンバ変数historyに格納されており、movesによって実装されている。moveshistoryから履歴を一手ずつ取ってきてJSXにしたものをリストにしている。historyは常に昇順であるためこれから作られたmovesもまた常に昇順となっている。つまり降順で表示したいときはmovesを逆順にすると良いと考えられる。

解答例

昇順、降順ボタンを任意の位置(ここでは着手履歴の上)に設定し、それを押すことで新たに追加するGameのメンバ変数ascendingの値を変える。ascendingは昇順でtrue、降順でfalseとなるようにする。ascendingの値によってrenderの直前にmovesを逆順にするかどうかを決める。

constructor(props) {
    super(props);
    this.state = {
        history: [
            {
                squares: Array(9).fill(null)
            }
        ],
        stepNumber: 0,
        xIsNext: true,
        ascending: true,
    };
}
render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

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

    let status;
    if (winner) {
        status = "Winner: " + winner;
    } else {
        status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    if(!this.state.ascending) moves.reverse();

    return (
        <div className="game">
            <div className="game-board">
                <Board
                    squares={current.squares}
                    onClick={i => this.handleClick(i)}
                />
            </div>
            <div className="game-info">
                <div>{status}</div>
                <button onClick={()=>this.setState({ascending: true})}>昇順</button>
                <button onClick={()=>this.setState({ascending: false})}>降順</button>
                <ol>{moves}</ol>
            </div>
        </div>
    );
}

一つ目はGameクラスのコンストラクタ部分で、二つ目はGameクラスのrender部分である。トグルボタンでは押したときにascendingをtrueやfalseを切り替えるように実装してやれば良い。

<ToggleButton onClick={()=>this.setState({ascending: !ascending})}/>

5.どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。

考え方

勝敗が決定したときにハイライトさせるので勝敗を確認するcalculateWinner関数と勝敗によって値が変わるGameクラス内のstatusを定義している部分をうまく変更すれば良いと考えられる。まず勝敗につながった3マスをどのように抜き出すかを考える。calculateWinner関数では勝利につながる可能性のある3マスの組を全列挙しそのうちの一つでも3マス全てが同じ記号であればその記号を返し、1つもなければnullを返している。つまり、calculateWinner関数では勝敗を決まった時勝利につながった3マスを特定することができ、返り値にその3マスも返すようにすることが可能であると考えられる。次に抜き出した情報をもとにハイライトを行う方法を考える。勝敗が決まった時のみこの処理を行うのでハイライトを行う機能はstatusを定義する場面で行う。勝敗が決まった時の処理でcurrent.squaresの先ほど抜き出した3マスをハイライトする機能を追加すると良いと考えられる。

解答例

まず、clulculateWinner関数の返り値を既存のものと勝利につながった3マスを合わせたものへと変更する。

function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return [squares[a], lines[i]];
        }
    }
    return [null, []];
}

返り値を変えたことによってhandleClickメソッドに少々の変更を行う。

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares)[0] || squares[i]) {//ここだよ
        return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
        history: history.concat([
            {
                squares: squares
            }
        ]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext
    });
}

最後にstatus内の処理に一手間加える。

let status;
if (winner[0]) {
    status = "Winner: " + winner[0];
    for(let i in winner[1]) current.squares[winner[1][i]] = <font color="#F00">{current.squares[winner[1][i]]}</font>;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

ハイライトは3つのマスを赤色にすることにした(これは何でもいい)。
しかし、この方法だと勝利につながったマスが5つの時であっても3つしかハイライトされない。このようなケースは通常人間同士が真剣に行ったときに出ることはないが、気持ち悪いので5つともハイライトされるようにする。変更したのはcalculateWinner関数のみ。

function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];
    let winner = null;
    let winLines = [];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            winLines = winLines.concat(lines[i]);
            winner = squares[a];
        }
    }
    return [winner, winLines];
}

6.どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

考え方

引き分けのメッセージはstatusに新たな分岐を作ることによって、引き分けの判別はcalculateWinner関数で引き換えの処理を加えればよいと考えられる。また、引き分けは勝敗が決まってない時のsquaresの中身にnullがあるかないかで判別することができる。

解答例

まず、statuscalculateWinner関数の返り値に依存しているので、calculateWinner関数を変更する。この関数はhandleClickメソッドにも使われており、それに影響が出ないような形で変更したい。つまり、bool値がtrueとなるような値になるようにする。

function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return squares[a];
        }
    }

    if (squares.indexOf(null) === -1) return "draw";

    return null;
}

最後にstatusの定義部分を変更する。引き分けになった時の表示はDrawにした。

let status;
if (winner === "draw") {
    status = "Draw";
} else if (winner) {
    status = "Winner: " + winner;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

おわりに(全体コード)

追加課題を全て終えた後はこのようなコードになった。より良い書き方や書き方の問題などがあれば追記するので教えていただきたい。

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

Reactのチュートリアル追加課題解いてみた

はじめに

Reactチュートリアルの追加課題の解答と、私が解いていく中で詰まったところを記述する。ReactやJSに関する知識が拙い中解いたので、より良い解き方などがあると考えられるが、一例として参考にしていただきたい。(より良い解き方など指摘いただければ追記致します。)
コードはチュートリアル内の最終結果を改変した。

1.履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。

考え方

着手の位置を履歴として残すために、historyに新たな要素として追加する。その後movesに着手の位置も表示するように変更すればよい。

解答例

まず、Gameクラスのコンストラクタにあるhistoryと盤面が動いた際の処理が書かれたメソッドhandleClick内のhistoryputStateをkeyとした着手の位置を保存するための要素を追加する(history自体ではなくhistory内にある辞書やhistoryに追加する辞書に対しての意)。
コンストラクタでは以下のように変更する(ここで定義しなくても動く)。

history: [
    {
        squares: Array(9).fill(null),
        putState: "(0, 0)"
    }
]

だが、handleClickメソッドでは着手の位置を与える必要がある。幸運なことに(当然だが)このメソッドは選択したマスの番号を引数として与えられているので、以下のように変更するだけでよい。

history: history.concat([
    {
        squares: squares,
        putState: ` (${i/3|0}, ${i%3})`
    }
])

JavaScriptのバージョンによっては文字列の埋め込みがこのようにできないので以下のようにする。

history: history.concat([
    {
        squares: squares,
        putState: " (" + (i/3|0).toString() + ", " + (i%3).toString() + ")"
    }
])

このようにして着手の位置を履歴として残せるようになったので最後にmovesを変更し、着手の位置を表示させる。

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

2.着手履歴のリスト中で現在選択されているアイテムをボールドにする。

考え方

選択されている盤面はGameクラスのメンバ変数StepNumberに格納されている。また、着手履歴の表示に関する部分はmovesに格納されている。このmovesは着手履歴を保存しているhistoryを用いてrenderされる度に生成される。さらに表示したい盤面を着手履歴のリストから選択するたびStepNumberが更新され再度renderされる。つまり、選択のたびrenderされるのでmovesが生成されるときにStepNumberと一致する番目にあるアイテムをボールドするようにすれば良いと考えられる。

解答例

変更点はmovesの定義部分のみ

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

最初falseの時のdesc{desc}と書いていてerror({}が二重になるため)を出していたのでこの部分は注意したい。

3.Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。

考え方

単純にリストを作って二重ループ回してリストにJSXを適宜格納し、ハードコーディング部分をリストに置き換える。

解答例

考え方通りコードを書く。

render() {
    const squareBoard = [];
    const row = 3;
    const col = 3;
    for(let i=0; i<row; i++){
        let rowBoard = [];
        for(let j=0;j<col; j++){
            rowBoard.push(this.renderSquare(j+3*i));
        }
        squareBoard.push(
            <div className="board-row">
                {rowBoard}
            </div>
        )
    }
    return (
        <div>
            {squareBoard}
        </div>
    );
}

これで上手くいくと考えていたが警告が出る。
Warning: Each child in a list should have a unique "key" prop.
これはkeyに関する警告でチュートリアル中にも触れられていたものである。keyを設定することでVirtualDOMのdiffから実際のDOMに反映させるときの変更を最小限にすることができる。変更を施すと以下のようになる。

class Board extends React.Component {
    renderSquare(i) {
        return (
            <Square
                value={this.props.squares[i]}
                onClick={() => this.props.onClick(i)}
                key={"square-"+i.toString()}
            />
        );
    }

    render() {
        const squareBoard = [];
        const row = 3;
        const col = 3;
        for(let i=0; i<row; i++){
            let rowBoard = [];
            for(let j=0;j<col; j++){
                rowBoard.push(this.renderSquare(j+3*i));
            }
            squareBoard.push(
                <div className="board-row" key={"row-"+i.toString()}>
                    {rowBoard}
                </div>
            )
        }
        return (
            <div>
                {squareBoard}
            </div>
        );
    }
}

ここでは"row-"+i.toString()としているがiだけでも問題ない(Squareも同様)。

4.着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。

私はトグルボタンそのものを実装することは本質ではなく、コードが複雑になると考えたので単純にボタンで実装した(トグルボタンにしたい方はトグルボタンのコンポーネントを作成してボタンのタグをトグルボタンのタグに変更することでできる)。

考え方

着手履歴は、Gameクラスのメンバ変数historyに格納されており、movesによって実装されている。moveshistoryから履歴を一手ずつ取ってきてJSXにしたものをリストにしている。historyは常に昇順であるためこれから作られたmovesもまた常に昇順となっている。つまり降順で表示したいときはmovesを逆順にすると良いと考えられる。

解答例

昇順、降順ボタンを任意の位置(ここでは着手履歴の上)に設定し、それを押すことで新たに追加するGameのメンバ変数ascendingの値を変える。ascendingは昇順でtrue、降順でfalseとなるようにする。ascendingの値によってrenderの直前にmovesを逆順にするかどうかを決める。

constructor(props) {
    super(props);
    this.state = {
        history: [
            {
                squares: Array(9).fill(null)
            }
        ],
        stepNumber: 0,
        xIsNext: true,
        ascending: true,
    };
}
render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

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

    let status;
    if (winner) {
        status = "Winner: " + winner;
    } else {
        status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    if(!this.state.ascending) moves.reverse();

    return (
        <div className="game">
            <div className="game-board">
                <Board
                    squares={current.squares}
                    onClick={i => this.handleClick(i)}
                />
            </div>
            <div className="game-info">
                <div>{status}</div>
                <button onClick={()=>this.setState({ascending: true})}>昇順</button>
                <button onClick={()=>this.setState({ascending: false})}>降順</button>
                <ol>{moves}</ol>
            </div>
        </div>
    );
}

一つ目はGameクラスのコンストラクタ部分で、二つ目はGameクラスのrender部分である。トグルボタンでは押したときにascendingをtrueやfalseを切り替えるように実装してやれば良い。

<ToggleButton onClick={()=>this.setState({ascending: !ascending})}/>

5.どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。

考え方

勝敗が決定したときにハイライトさせるので勝敗を確認するcalculateWinner関数と勝敗によって値が変わるGameクラス内のstatusを定義している部分をうまく変更すれば良いと考えられる。まず勝敗につながった3マスをどのように抜き出すかを考える。calculateWinner関数では勝利につながる可能性のある3マスの組を全列挙しそのうちの一つでも3マス全てが同じ記号であればその記号を返し、1つもなければnullを返している。つまり、calculateWinner関数では勝敗を決まった時勝利につながった3マスを特定することができ、返り値にその3マスも返すようにすることが可能であると考えられる。次に抜き出した情報をもとにハイライトを行う方法を考える。勝敗が決まった時のみこの処理を行うのでハイライトを行う機能はstatusを定義する場面で行う。勝敗が決まった時の処理でcurrent.squaresの先ほど抜き出した3マスをハイライトする機能を追加すると良いと考えられる。

解答例

まず、clulculateWinner関数の返り値を既存のものと勝利につながった3マスを合わせたものへと変更する。

function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return [squares[a], lines[i]];
        }
    }
    return [null, []];
}

返り値を変えたことによってhandleClickメソッドに少々の変更を行う。

handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares)[0] || squares[i]) {//ここだよ
        return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
        history: history.concat([
            {
                squares: squares
            }
        ]),
        stepNumber: history.length,
        xIsNext: !this.state.xIsNext
    });
}

最後にstatus内の処理に一手間加える。

let status;
if (winner[0]) {
    status = "Winner: " + winner[0];
    for(let i in winner[1]) current.squares[winner[1][i]] = <font color="#F00">{current.squares[winner[1][i]]}</font>;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

ハイライトは3つのマスを赤色にすることにした(これは何でもいい)。
しかし、この方法だと勝利につながったマスが5つの時であっても3つしかハイライトされない。このようなケースは通常人間同士が真剣に行ったときに出ることはないが、気持ち悪いので5つともハイライトされるようにする。変更したのはcalculateWinner関数のみ。

function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];
    let winner = null;
    let winLines = [];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            winLines = winLines.concat(lines[i]);
            winner = squares[a];
        }
    }
    return [winner, winLines];
}

6.どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

考え方

引き分けのメッセージはstatusに新たな分岐を作ることによって、引き分けの判別はcalculateWinner関数で引き換えの処理を加えればよいと考えられる。また、引き分けは勝敗が決まってない時のsquaresの中身にnullがあるかないかで判別することができる。

解答例

まず、statuscalculateWinner関数の返り値に依存しているので、calculateWinner関数を変更する。この関数はhandleClickメソッドにも使われており、それに影響が出ないような形で変更したい。つまり、bool値がtrueとなるような値になるようにする。

function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6]
    ];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return squares[a];
        }
    }

    if (squares.indexOf(null) === -1) return "draw";

    return null;
}

最後にstatusの定義部分を変更する。引き分けになった時の表示はDrawにした。

let status;
if (winner === "draw") {
    status = "Draw";
} else if (winner) {
    status = "Winner: " + winner;
} else {
    status = "Next player: " + (this.state.xIsNext ? "X" : "O");
}

おわりに(全体コード)

追加課題を全て終えた後はこのようなコードになった。より良い書き方や書き方の問題などがあれば追記するので教えていただきたい。

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

Reactアプリケーション開発入門

■create-react-appのインストール&Hello Worldの表示

create-react-appはFacebookが提供しているCLIツールです。
環境構築の手間をスキップして学習に集中できます。

// create-react-appのインストール
yarn global add create-react-app

// Reactアプリケーションの作成
create-react-app hello-world

// hello-worldに移動
cd hello-world

// 開発開始
yarn start

7b695480-013f-11eb-82ee-a6989fc53549.png

ファイル構造はこんなかんじです。
7b695480-013f-11eb-9223-07d42cd3a344.png

Hello Worldを表示するにはsrc/App.js内をこのように書き換えます。

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code>and save to reload {/* この行をHello Worldに書き換える */}
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

これでHello Worldが表示されました。
7c01eb00-013f-11eb-8f31-3b24ac4893e5.png

ちなみにこのreturn()内の部分はぱっと見HTMLに見えますが、実際はJSXというものになっています。
詳細は公式をご確認ください。( https://ja.reactjs.org/docs/introducing-jsx.html )

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  //このreturnの中がJSX
  return ( 
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Hello World
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

■コンポーネントについて

Reactのコンポーネントには「関数コンポーネント」と「クラスコンポーネント」の二種類があります。

<関数コンポーネント>

実は先ほどのHello Worldを表示した時のApp.jsが既に関数コンポーネントの形になっていました。

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Hello World
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

わかりやすく余計な部分を省いた形が下記になります。
※ちなみにですが、JSXが内部的にReactを使用しているため「import React from 'react';」の記述を無くすとエラーになります。

import React from 'react';

function FunctionComponentA() {
  return (
    <div className="function-component_A">
      function-component_Aです
    </div>
  );
}

export default FunctionComponentA;

このようにsrc/index.jsに追加することで反映されます。

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import FunctionComponentA from './FunctionComponentA' //ここに追加
import * as serviceWorker from './serviceWorker';

ReactDOM.render(
  <React.StrictMode>
    <FunctionComponentA /> {/* ここに追加 */}
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

7c9a8180-013f-11eb-8449-abdbaf2c3e57.png

このようにアロー関数の形で定義することもできます。

import React from 'react';

const FunctionComponentB = () => {
  return (
    <div className="function-component_B">
      function-component_Bです
    </div>
  );
}

export default FunctionComponentB;

<クラスコンポーネント>

クラスコンポーネントはこのように定義します。

import React, { Component } from 'react';

class ClassComponent extends Component {
  render() {
    return (
      <div className="class-component">
        ClassComponentです
      </div>
    )
  }
}

export default ClassComponent;

Componentを継承したクラスとして定義します。

<関数コンポーネントとクラスコンポーネントそれぞれの特徴>

関数コンポーネント
・簡潔に書けるので基本はこちらが望ましい。
・状態管理によるレンダー制御ができない。

クラスコンポーネント
・状態管理によるレンダー制御が可能。
・ローカルstateやライフサイクルメソッドが利用可能。

■props

こちらが見本になります。

import React from 'react';

const Parent = () => {
  return (
    <div>
      <strong>表示されるとこうなります</strong>
      <Child name={"ポール・マッカートニー"} instrument={"ベース"} />
      <Child name={"ジョン・レノン"} instrument={"ギター"} />
      <Child name={"ジョージ・ハリスン"} instrument={"ギター"} />
      <Child name={"リンゴ・スター"} instrument={"ドラム"} />
    </div>
  )
}

const Child = (props) => {
  return (
    <div>僕は{props.name}です担当は{props.instrument}です</div>
  )
}

export default Parent;

7dcbae80-013f-11eb-8c85-0e24cc520e73.png

Child(子コポーネント)をParent(親コンポーネント)で利用する際に属性のような形でpropsを渡すことができます。
渡ったpropsはChild側で受け取り、利用するという流れになります。

データを配列で一括に渡したいと言う時には、「map」を利用することで実現可能です。
下記キャプチャでは、配列ビートルズをmapで展開しています。

import React from 'react';

const Parent2 = () => {
  const beatles = [
    {
      name: "ポール・マッカートニー",
      instrument: "ベース"
    },
    {
      name: "ジョン・レノン",
      instrument: "ギター"
    },
    {
      name: "ジョージ・ハリスン",
      instrument: "ギター"
    },
    {
      name: "リンゴ・スター",
      instrument: "ドラム"
    }
  ]
  return (
    <div>
      <strong>表示されるとこうなります(map)</strong>
      {
        beatles.map((beatlesMember, index) => {
          return <Child name={beatlesMember.name} instrument={beatlesMember.instrument} key={index} />
        })
      }
    </div>
  )
}

const Child = (props) => {
  return (
    <div>僕は{props.name}です担当は{props.instrument}です</div>
  )
}

export default Parent2;

■prop-types(型チェック)

こちらが見本になります。

import React from 'react';
import PropTypes from 'prop-types' //これをimportします。

const Parent = () => {
  return (
    <div>
      <strong>表示されるとこうなります</strong>
      <Child name={"ポール・マッカートニー"} year={1942} guitar={true} />
      <Child name={"ジョン・レノン"} year={1940} guitar={true} />
      <Child name={"ジョージ・ハリスン"} year={1943} guitar={true} />
      <Child name={"リンゴ・スター"} year={1940} guitar={false} />
    </div>
  )
}

const Child = (props) => {
  return (
  <div>僕は{props.name}です{props.year}年生まれです{props.guitar ? 'ギターを弾けます。' : 'ギターは弾けません。'}</div>
  )
}

//ここでpropsに対しての型定義を行っています。
Child.propTypes = {
  name: PropTypes.string,
  year: PropTypes.number,
  guitar: PropTypes.bool
}

export default Parent;

prop-typesを導入する手順としては、
1. PropTypesをimportする。
2. コンポーネントのpropsに対して型定義を行う。
になります。

●動作確認
「name="リンゴ・スター"」の部分のyearとguitarの値を文字列に変えてみます。

import React from 'react';
import PropTypes from 'prop-types' //これをimportします。

const Parent = () => {
  return (
    <div>
      <strong>表示されるとこうなります</strong>
      <Child name={"ポール・マッカートニー"} year={1942} guitar={true} />
      <Child name={"ジョン・レノン"} year={1940} guitar={true} />
      <Child name={"ジョージ・ハリスン"} year={1943} guitar={true} />
      <Child name={"リンゴ・スター"} year={"1940"} guitar={"false"} /> {/* ここの値を間違ったもの(文字列)に変更 */}
    </div>
  )
}

const Child = (props) => {
  return (
  <div>僕は{props.name}です{props.year}年生まれです{props.guitar ? 'ギターを弾けます。' : 'ギターは弾けません。'}</div>
  )
}

//ここでpropsに対しての型定義を行っています。
Child.propTypes = {
  name: PropTypes.string,
  year: PropTypes.number,
  guitar: PropTypes.bool
}

export default Parent;

7efcdb80-013f-11eb-8bb6-08f6fd378bcd.png

無事にエラーが吐き出されました。

■state

こちらが見本になります。
countというstateを持ったCounterコンポーネントを作成しました。

import React, { Component } from 'react';

class Counter extends Component {
  // 状態を持たせるための処理
  constructor(props) { // コンポーネントの初期化時に呼び出される
    super(props)
    // this.stateでstateにアクセスできる。
    this.state = { count: 0 }
  }

  countUp = () => {
    this.setState({ count: this.state.count + 1 }) // stateを変更する時は必ずsetState()を使用する。
  }

  countDown = () => {
    this.setState({ count: this.state.count - 1 }) // stateを変更する時は必ずsetState()を使用する。
  }

  render() {
    return (
      <div className="counter">
        <div>count:{ this.state.count }</div>
        <button onClick={this.countUp}>+1</button>
        <button onClick={this.countDown}>-1</button>
      </div>
    )
  }
}

export default Counter;

7f957200-013f-11eb-9bd5-315fd9447874.png

以下、ポイントになります。

・レンダー制御が必要なのでクラスコンポーネントで作成した。
・stateの値を変更する時はsetState()を必ず使用する。
・this.stateでstateにアクセスする。

:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 16日目 Full Static Generationを試す @AsaToBan

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

【保存版】Docker × React × Railsで環境構築していく方法

はじめに

本記事へのアクセスありがとうございます。
投稿主はプログラミング初心者であり、この方法が「最適解」かは分かりません。
しかし、動作は検証済みであり同様な記事も確認できたので信憑性はあると思います。

記事通りにコピペしていくだけで環境構築できますので、説明がいらない人はコードだけをコピペして行ってください。

想定読者

  • Dockerインストール済み
  • Docker初心者
  • フロントエンド側とバックエンド側の開発環境を分けて構成したい
  • 現在 ( 2020年12月 )にある同様なQiita記事でエラーで詰まってしまっている

最終ファイル構成

qiita_docker1.png

  • apiの中にRailsファイルを格納されています。
  • frontの中にReactファイルを格納されています。

さっそくスタート

初期ファイルを用意する

qiita_docker2.png
apiの中にはDockerfile , entrypoint.sh , Gemfile , Gemfile.lockの4つを作成する。
Gemfile.lockは何も記述しないファイルとする。

docker-compose.ymlの記述

docker-compose.ymlを記述していきます

docker-compose.yml
version: '3'
services:
  db:
    image: postgres:12.3
    volumes:
      - postgres-data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=password
  api:
    build:
      context: ./api/
      dockerfile: Dockerfile
    command:  /bin/sh -c "rm -f /myapp/tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    image: rails:dev
    volumes:
      - ./api:/myapp
      - ./api/vendor/bundle:/myapp/vendor/bundle
      - ./api/vendor/node_modules:/myapp/vendor/node_modules
    environment:
      TZ: Asia/Tokyo
      RAILS_ENV: development
    ports:
      - "3000:3000"
    depends_on:
      - db
  front:
    build:
      context: ./front/
      dockerfile: Dockerfile
    volumes:
      - ./front:/usr/src/app
    command: sh -c "cd react-sample && yarn start"
    ports:
      - "8000:3000"
volumes:
  postgres-data:
    driver: local
  bundle:
  node_modules:

api / Dockerfileの記述

Dockerfileを記述していきます

FROM ruby:2.7
RUN apt-get update -qq && apt-get install -y nodejs postgresql-client yarnpkg
RUN ln -s /usr/bin/yarnpkg /usr/bin/yarn
RUN mkdir /myapp
WORKDIR /myapp
COPY Gemfile /myapp/Gemfile
COPY Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

api / entrypoint.shの記述

entrypoint.shを記述していきます

entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

api / Gemfileの記述

Gemfileを記述していきます

source 'https://rubygems.org'
gem 'rails', '~>6'

front / Dockerfileの記述

Dockerfileを記述していきます

FROM node:10-alpine
RUN mkdir /myapp
WORKDIR /usr/src/app

node:10以上出ないと後々にcreate-react-app出来ないので注意してください

コマンドを実行する

まずは以下の3つのコマンドをターミナルで入力してください

$ docker-compose run api rails new . --force --no-deps --database=postgresql --api
$ docker-compose build
$ docker-compose run --rm front sh -c "npm install -g create-react-app && create-react-app react-sample"

api/config/database.ymlを下記のように書き換えてください

api/config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  host: db
  username: postgres
  password: password
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

次に以下のコマンドをターミナルで入力してください

$ docker-compose up

以下のコマンドを現在まで使用しているターミナルとは別のターミナル(新規作成)で入力してください

$ docker-compose run api rake db:create

以上で環境構築が完了です。

おわりに

この状況で...
localhost:3000にアクセスすると、Rails用のページにアクセスします。
localhost:8000にアクセスすると、React用のページにアクセスします。

お疲れ様でした。

少しでも役に立ったと思う方がいましたらLGTMをお願いします?‍♂️

おまけ

Docker内で開発するときは以下のコマンドを利用します。

docker-compose run web bundle exec rails g コマンド
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React+TypeScript環境にESLint+Prettierを導入する

みなさま初めまして。
ニジボックスに入社し、フロントエンドエンジニアを名乗り始めてから約1年が経ちました。

4ヶ月前くらいまで、Lintって何!?って感じであんまり良さを感じていなかった(というか何者か分かっていなかった)(というか既に案件のリポジトリには導入されていたので何も考えずに使っていた)ので、せっかくの機会なので調査してみました。
React+TypeScript環境にESLint+Prettierを導入するまでの手順にして備忘としてまとめてみます。

create-react-app

create-react-appを使えばコマンド一つでReactを書き始めることができます。
--template typescriptオプションを付けなければJSでも書けます。
ts-appの部分はお好きなプロジェクト名に変更してください。

npx create-react-app ts-app --template typescript

Prettier

Prettierはコードを自動整形するツールです。
ESLintにもコードフォーマットの機能はあるんですが、Prettierの方がコードフォーマットとしては優秀なようなので、ESLintとPrettierを併用するのが主流のようです。
なにはともあれ、Prettierをインストールしましょう。

npm install --save-dev prettier

Prettierの設定ファイルを作成

.prettierrc.yml
---
printWidth: 80
tabWidth: 2
singleQuote: true
trailingComma: none
jsxBracketSameLine: true
semi: false
arrowParens: avoid
  • printWidth 折り返しをする行の長さを指定(公式曰くおすすめは80文字なので従います)
  • semi 行の最後にセミコロンをつけるかどうか指定
  • arrowParens 引数が1つの時にはカッコを省略するかどうか

などなど、公式に指定できるオプションは詳しく載っています。この辺りはお好みでどうぞ。

いったん動作確認

package.jsonに以下のscriptを追加してきちんと動作するか確認してみます。

package.json
{
  "scripts": {
    "fix:prettier": "prettier --write src",
    "lint:prettier": "prettier --check src"
  }
}

ESLint

これってなんて読んでますか?
ミスチル的には「エス」なんですが、やっぱり「イーエス」が正しいんでしょうか。

ちなみにESLintは、JavaScriptのための静的検証ツールで、コードを実行する前に明らかなバグを見つけたり、括弧やスペースの使い方などのスタイルを統一するために使用されます。
先述の通り、ESLintでもコードを自動的に修正することはできますが、自動整形という点においてはPrettierの方が優れているため、コードの構文チェックはESLintが行い、コードの整形はPrettierに任せたいと思います。

ただし、この自動整形は、括弧が足りなかったり、カンマが足りなかったりした場合に自動で追加してくれるなんてことはないので、そこは自分で整えてあげる必要があります。

では以下のコマンドを実行して、ESLintを導入します。

npx eslint --init
ESLintをどのように使用しますか
? How would you like to use ESLint? … 
  To check syntax only
  To check syntax and find problems
❯ To check syntax, find problems, and enforce code style
あなたのプロジェクトはどんな種類のモジュールを使っていますか
❯ JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these
プロジェクトで使用しているフレームワークはどれですか
? Which framework does your project use? … 
❯ React
  Vue.js
  None of these
プロジェクトはTypeScriptを使用していますか
? Does your project use TypeScript? › Yes
コードはどこで実行されますか
? Where does your code run? …  (Press <space> to select, <a> to toggle all, <i> to invert selection)
✔ Browser
  Node
プロジェクトのスタイルをどのように定義しますか
? How would you like to define a style for your project? … 
❯ Use a popular style guide
  Answer questions about your style
  Inspect your JavaScript file(s)
どのスタイルガイドに従いたいですか
? Which style guide do you want to follow? … 
❯ Airbnb: https://github.com/airbnb/javascript
  Standard: https://github.com/standard/standard
  Google: https://github.com/google/eslint-config-google
設定ファイルをどの形式にしますか
? What format do you want your config file to be in? … 
❯ JavaScript
  YAML
  JSON
関連する必要なパッケージをnpmでインストールするか
? Would you like to install them now with npm? › Yes

パッケージを全てインストールし終わったら、
追加で、Prettierとの連携用のパッケージを導入します。

npm install --save-dev eslint-config-prettier eslint-plugin-prettier
  • eslint-config-prettier
    Prettierと競合する可能性のあるESLintのルールをすべてオフにする

  • eslint-plugin-prettier
    ESLint上でPrettierを動作させるために使用する

ESLintの設定ファイルを編集する

インストールが終わったら、ルートディレクトリに.eslintrc.jsファイルが作成されているのでこちらを編集していきます。

.eslintrc.js
module.exports = {
  // ソースコードが対象としている環境
  // ここの指定によって使えるクラスや関数の種類が変わる
  env: {
    browser: true,
    es2021: true
  },
  // 設定をまとめて変更する場合はextendsに記述する
  // 配列の0番目から順番に上書きされるため、Prettier関連の記述は最後に追加する
  extends: [
    'airbnb',
    'airbnb/hooks',
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
    'prettier/@typescript-eslint'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: 12,
    sourceType: 'module'
  },
  // 機能を追加するプラグインを記述する
  plugins: ['react', '@typescript-eslint', 'prettier'],
  // プロジェクト内部で個別に機能を切り替えたい時はrulesに記述する
  rules: {
    'no-use-before-define': 'off',
    '@typescript-eslint/no-use-before-define': ['error'],
    'react/jsx-filename-extension': 'off',
    'react/jsx-closing-bracket-location': [
      'error',
      { selfClosing: 'tag-aligned', nonEmpty: 'after-props' }
    ],
    'react/jsx-one-expression-per-line': 0,
    'import/no-unresolved': ['off', { caseSensitive: true }],
    'import/extensions': 'off'
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
}

  • extendsは、ESLintのルールのON/OFFを設定するもので、例えば'eslint:recommended'は、推奨設定(チェックマークがついているもの)を全てONにする役割を持っています。それが、配列の0番目から上書きされていきます。

  • pluginsは、ルールそのものを追加できます。
    上に入っていないものでも、例えば

    • eslint-plugin-immutable JavaScriptのすべてのミューテーションを無効にするESLintプラグインです。今回は入れていませんが、Reactで書くなら入れた方が良さそうな気がする
    • eslint-plugin-import import/export構文をサポートし、ファイルパスとインポート名のスペルミスの問題を防ぐことができるのでぜひ入れたい

いったん動作確認(パート2)

ESLint単体でもきちんと動作するか確認しておきましょう。

package.json
{
  "scripts": {
    "fix": "run-s fix:prettier fix:eslint",
    "fix:eslint": "eslint src --ext .ts,tsx --fix",
    "lint": "run-p lint:prettier lint:eslint",
    "lint:eslint": "eslint src --ext .ts,tsx",
  }
}

VSCode

VSCodeで使用する場合、保存時に自動整形されるように設定ファイルを作成します。
以下のように記述することで、ESLintのルールで自動整形されるようになります。

.vscode/extensions.json
{
  "recommendations": [
    "streetsidesoftware.code-spell-checker"
  ],
  "unwantedRecommendations": []
}
.vscode/settings.json
{
  "eslint.lintTask.enable": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

また、tsconfig.jsonで下記のようなエラーが出ている場合があります。
Cannot use JSX unless the '--jsx' flag is provided

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx"
           ~~~~~~~~~~~
  },
}

VSCodeで古いバージョンのTypeScriptを使用している可能性があるので、以下の手順でバージョンを変更します。

  • コマンドパレットに移動(Control + Shift + P)
  • 「TypeScript:TypeScriptバージョンを選択...」を選択
  • 「ワークスペースバージョンを使用」を選択(4.1.3)

tsconfig.jsonは、TypeScriptで記述されたコードをJavaScriptにコンパイルする際のルールが記述されたファイルです。
詳しくは公式へどうぞ。

おまけ

ESLintの警告を特定のコードだけ除外したい場合

変数名の規則はcamelCaseだが、サーバーのレスポンスのみsnake_caseを許容したい場合など、特定の行やブロック内のみ警告を除外したい場合には、下記のようにコメントを入れることで無効化できます。

// eslint-disable-next-line camelcase
const { status_code } = await res.json();
/* eslint-disable camelcase */

const { status_code } = await res.json();

/* eslint-enable camelcase */

以上、ここまで来れば、あなたがエディター上でどんなに汚いコードを書いたとしても、Lintが綺麗に整頓してくれるようになっているでしょう?
みなさま良いクリスマスを〜

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

【React/Go】ユーザー認証をCognito + Amplifyで構築してみた ~ユーザ登録/削除編~

はじめに

Reactで作成したWebアプリケーションのユーザー認証部分をCognito + Amplifyフレームワークで構築してみました。構築の基本部分については「【React】ユーザー認証をCognito + Amplifyで構築してみた」の構築準備編構築完成編をご覧ください。
本記事は、アプリケーションからユーザーを登録、削除する方法についてまとめています。

完成画面

今回は、アプリケーションからユーザーを登録すると、ユーザープールとDBそれぞれにユーザーが登録されて、画面には「ユーザーを登録しました。」というアラートが出力されるようにします。

スクリーンショット 2020-12-10 19.15.23.png

[Submit]をクリックすると↓↓↓

画面
スクリーンショット 2020-12-10 19.15.11.png

Cognitoユーザープール管理画面
スクリーンショット 2020-12-06 18.55.55.png

DB(userテーブル)※一部抜粋

+----+-----------+------------------+
| id | user_name | email            |
+----+-----------+------------------+
|  2 | test      | test@example.com |
+----+-----------+------------------+

方法検討

要件

構築方法を考えるにあたり、条件は以下の通りです。

  • 静的コンテンツをS3に置いている
  • アプリケーション部分はLambda + RDS Proxy + RDSで実装している
  • ユーザーデータはCognitoユーザープール以外に、RDSに保存している
  • NATゲートウェイはコストが高いので使いたくない

現在の構成図(ユーザー認証付加前)

ユーザー認証付加前のアプリケーション部分の構成図は下記の通りです。

Cognito-before.png

VPC Lambdaによる弊害

ここで、LambdaをVPC内に設置していることで、Cognitoにアクセスできないことに気付きました。パブリックサブネットに置いているんだから、アクセスできると勝手に思っていました。

AWS開発者ガイドによると、次のように説明されています。

プライベートリソースにアクセスするには、関数をプライベートサブネットに接続します。関数にインターネットアクセスが必要な場合は、ネットワークアドレス変換 (NAT) を使用します。関数をパブリックサブネットに接続しても、インターネットアクセスやパブリック IP アドレスは提供されません。

Lambda関数をパブリックサブネットに接続しても、インターネットアクセスやパブリック IP アドレスは提供されないんです。NATゲートウェイを使用する場合にもLambda関数はプライベートサブネットに置くべきだそうです。パブリックサブネットにLambdaを置いておくメリットはなさそうなので、VPC Lambdaはプライベートサブネットに置きましょう!!!

結論

この条件に沿ってアプリケーションの登録、削除処理を考えた結果、VPC Lambdaをプライベートサブネットに移動させ、NATゲートウェイは使いたくないので、強引にLambdaからLambdaを呼び出すことにしました。

シーケンス図

シーケンス図を書くと次のようになります。

スクリーンショット 2020-12-15 20.00.08.png

構成図(ユーザー認証付加後)

構成は下図の通りになりました。

Cognito-after (1).png

手順

下記の流れで進めていきます。

  1. RDSを更新するLambda関数:Lambda(VPC) の作成
  2. Cognitoを更新するLambda関数:Lambda(非VPC) の作成
  3. API Gatewayの作成
  4. フロントの実装

ユーザーを登録する

1. DBを更新するLambda関数:Lambda(VPC) の作成

Lambda(非VPC)の作成時につけるIAMロールにLambda(VPC)のarnが必要なので、先にLambda(VPC)から作成します。
VPC内に設置してRDSに情報を書き込むLambdaを作成していきます。このLambdaに関しては、RDSにデータが保存できれば良く、特に既存のLambdaと変わりないので割愛します。
祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみたの「8. Lambda関数の作成」を参考に作成しました。

ソースコード

ソースコードはこのような感じです。
※↓クリックするとソースコードが見れます。

ソースコード
lambda_vpc.go
package main

import (
    "database/sql"
    "fmt"
    "github.com/aws/aws-lambda-go/lambda"
    _ "github.com/go-sql-driver/mysql"
    "os"
)

type MyEvent struct {
    UserName string `json:"userName"`
    Email    string `json:"email"`
}

// os.Getenv()でLambdaの環境変数を取得
var dbEndpoint = os.Getenv("dbEndpoint")
var dbUser = os.Getenv("dbUser")
var dbPass = os.Getenv("dbPass")
var dbName = os.Getenv("dbName")

func RDSConnect() (*sql.DB, error) {
    connectStr := fmt.Sprintf(
        "%s:%s@tcp(%s:%s)/%s?charset=%s",
        dbUser,
        dbPass,
        dbEndpoint,
        "3306",
        dbName,
        "utf8",
    )
    db, err := sql.Open("mysql", connectStr)
    if err != nil {
        return nil, err
    }
    return db, nil
}

func RDSProcessing(event MyEvent, db *sql.DB) (interface{}, error) {

    tx, err := db.Begin()
    if err != nil {
        return nil, err
    }
    defer tx.Rollback()

    // ユーザーテーブルに情報を登録
    stmt, err := tx.Prepare("INSERT INTO user(user_name, email) VALUES (?, ?) ")
    if err != nil {
        return nil, err
    }
    defer stmt.Close()

    if _, err := stmt.Exec(event.UserName, event.Email); err != nil {
        return nil, err
    }

    if err := tx.Commit(); err != nil {
        return nil, err
    }

    response := "正常に処理が完了しました。"
    return response, nil
}

func run(event MyEvent) (interface{}, error) {
    fmt.Println("RDS接続 start!")
    db, err := RDSConnect()
    if err != nil {
        fmt.Println("DBの接続に失敗しました。")
        panic(err.Error())
    }
    fmt.Println("RDS接続 end!")
    fmt.Println("RDS処理 start!")
    response, err := RDSProcessing(event, db)
    if err != nil {
        fmt.Println("DB処理に失敗しました。")
        panic(err.Error())
    }
    fmt.Println("RDS処理 end!")
    return response, nil
}

/**************************
   メイン
**************************/
func main() {
    lambda.Start(run)
}

2. Cognitoを更新するLambda関数:Lambda(非VPC) の作成

VPCの外に置いて、Cognitoユーザープールへの登録とRDSを更新するLambda(VPC)を実行するLambdaを作成していきます。

IAMロール

下記の2つの権限をつけたポリシーを作成してアタッチします。

  • Cognitoユーザープールにユーザーを登録/削除する権限
  • Lambda(VPC)を実行する権限
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cognito-idp:AdminDeleteUser",
                "cognito-idp:AdminCreateUser"
            ],
            "Resource": "<Cognitoのarn>"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "<Lambda(VPC)のarn>"
        }
    ]
}

ソースコード

lambda_no_vpc.go
package main

import (
    "encoding/json"
    "fmt"
    "os"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
    "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface"
    l "github.com/aws/aws-sdk-go/service/lambda"
    "github.com/aws/aws-sdk-go/service/lambda/lambdaiface"
)

type MyEvent struct {
    UserName string `json:"userName"`
    Email    string `json:"email"`
}

func AddCognitoUser(svc cognitoidentityprovideriface.CognitoIdentityProviderAPI, event MyEvent) error {

    // 登録時にユーザーにメール送信
    desiredDeliveryMediums := []*string{aws.String("EMAIL")}

    // メールアドレスとメールアドレス検証済みを設定
    userAttributes := []*cognitoidentityprovider.AttributeType{
        {
            Name:  aws.String("email"),
            Value: aws.String(event.Email),
        },
        {
            Name:  aws.String("email_verified"),
            Value: aws.String("true"),
        },
    }

    // ユーザープールの設定
    // os.Getenv()でLambdaの環境変数を取得
    userPoolId := aws.String(os.Getenv("userPoolId"))

    // ユーザー名の設定
    username := aws.String(event.UserName)

    // Inputの作成
    input := &cognitoidentityprovider.AdminCreateUserInput{}
    input.DesiredDeliveryMediums = desiredDeliveryMediums
    input.UserAttributes = userAttributes
    input.UserPoolId = userPoolId
    input.Username = username

    // 処理実行
    result, err := svc.AdminCreateUser(input)
    if err != nil {
        fmt.Println(err.Error())
        return err
    }

    fmt.Println(result)
    return nil
}

func AddDbUser(svc lambdaiface.LambdaAPI, event MyEvent) error {

    // ValidateLambdaに送る情報の作成
    jsonBytes, _ := json.Marshal(event)

    // Inputの作成
    input := &l.InvokeInput{}
    input.FunctionName = aws.String(os.Getenv("arn"))
    input.Payload = jsonBytes
    input.InvocationType = aws.String("RequestResponse") // 同期実行

    // 処理実行
    result, err := svc.Invoke(input)
    if err != nil {
        fmt.Println(err.Error())
        return err
    }

    fmt.Println(result)
    fmt.Println(string(result.Payload))

    return nil
}

func run(event MyEvent) (interface{}, error) {
    fmt.Println("Cognito登録 start!")
    // セッション作成
    csvc := cognitoidentityprovider.New(session.Must(session.NewSession()))

    if err := AddCognitoUser(csvc, event); err != nil {
        fmt.Println("ユーザー登録に失敗しました。")
        panic(err.Error())
    }
    fmt.Println("Cognito登録 end!")

    fmt.Println("db登録 start!")
    // セッションの作成
    lsvc := l.New(session.Must(session.NewSession()))

    if err := AddDbUser(lsvc, event); err != nil {
        fmt.Println("ユーザー登録に失敗しました。")
        panic(err.Error())
    }
    fmt.Println("db登録 end!")

    fmt.Println("end!")
    response := "正常に処理が完了しました。"
    return response, nil
}

/**************************
   メイン
**************************/
func main() {
    lambda.Start(run)
}

3. API Gatewayの作成

REST APIでPOSTメソッドを作成し、Lambda(非VPC)を紐付けます。
特に特別な設定は不要なので省略します。

4. フロントの実装

登録画面を作成します。今回は、ユーザー名とメールアドレスが必須項目なので、その2つを登録できる入力欄と登録ボタンを簡単に作成しています。登録が完了すると「ユーザーを登録しました。」というアラートが出ます。

axiosのインストール

API Gatewayを叩くのにaxiosを使うために、プロジェクトにaxiosを追加します。

$ yarn add axios

ソースコード

axiosの使い方はaxiosライブラリを使ってリクエストするを参考にしました。

RegistrationForm.js
import React from "react";
import axios from "axios";


function RegistrationForm() {
    const API_ADD_URL = "<API Gatewayで取得したURL>"
    const [userName, setUserName] = React.useState("");
    const [email, setEmail] = React.useState("");

    const handleNameChange = event => {
        setUserName(event.target.value);
    };

    const handleEmailChange = event => {
        setEmail(event.target.value);
    }

    const handleSubmit = event => {
        axios.post(API_ADD_URL, {userName: userName, email: email})
            .then((response) => {
                if(response.data === "正常に処理が完了しました。"){
                    alert("ユーザーを登録しました。")
                    console.log(response);
                } else {
                    throw Error(response.data.errorMessage)
                }
            }).catch((response) => {
            alert("登録に失敗しました。もう一度登録してください。");
            console.log(response);
        });
        event.preventDefault();
    }

    return (
        <div>
            <h2>ユーザー登録</h2>
            <form onSubmit={handleSubmit} >
                <label >
                    ユーザー名:
                    <input type="text" value={userName} onChange={handleNameChange} /><br/>
                </label>
                <label >
                    Eメール:
                    <input type="text" value={email} onChange={handleEmailChange} /><br/>
                </label>
                <input type="submit" value="Submit" />
            </form>
        </div>
    );
}

export default RegistrationForm;

ユーザーを削除する

ユーザーから削除する場合も、基本的に登録するのと同じです。ユーザープールから削除するにはSDKのAdminDeleteUserを使用します。

実行結果

無事冒頭の完成画面のように動くようになりました!

fh4o1-lwya2.gif

おわりに

LambdaからLambdaを実行することで、NATゲートウェイを使わずにCognitoとDBの両方にユーザーを登録することができました!今考えると、Cognitoユーザープールに登録するのはAmplifyでAdmin Queries APIを使うようにして、DBに保存するのは既存のようにLambdaを呼び出すようにするのでも良かったかなとも思います!
次回は、サインインページにある、使用しないアカウント作成ボタンを消したいと思います!

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

react-draggableにクリックイベントを追加する

この記事はReact #2 Advent Calendar 2020の16日目の記事です。

はじめに

この記事ではreact-draggbleでクリックイベントを追加する方法についてまとめています。
react-draggableの情報は少なく、自分は苦労したので、少しでも参考になれば幸いです。

react-draggableとは

github:https://github.com/STRML/react-draggable
デモ
Reactでドラッグ処理を実現する為のライブラリです。

まずはドラッグを実現する

957bebf43c202b6e7ac395836fac2881.gif
サンプルコード:https://codesandbox.io/s/red-platform-3q7iv?file=/src/components/piece.tsx
react-draggableを素直に使ってドラッグを実現しました。
ポイントをいくつか解説していきます。

ドラッグしたい要素を<Draggable>でラップする

3a8b3cf0e04c5d6148c73214e44da60b.png

<Draggable>要素は既存の要素をラップし、新しいイベントハンドラとスタイルでそれを拡張します。DOMにラッパー要素は作成されません。

とドキュメントにあるので、まずは<Draggable>で要素をラップします。
Propsにはx、yの座標とonDragイベントを渡しています。

ドラッグイベントでstateを更新して、要素の座標を変える

7e2524d4c416722cab071f91e099d202.png
useStateで座標の状態を定義します。
onDragイベントでは<Draggable>のdataから得た座標情報でstateを更新しています。
これによりドラッグで要素の座標が変わります

<Draggable>の型とProps

<Draggable>の型とPropsはドキュメントではこの様になっています。(https://github.com/STRML/react-draggable#draggable-props)
今回使った箇所はこの辺りです。
137dfd649182d3d524a97c62d49e2f6a-1.png
onDragイベントの型はDraggbleEventHandlerです。引数で取っているdataの方で座標の更新に使ったlastXとlastYが定義されています。

ドラッグ+クリックを実現する

こっから本題です。
先ほど作ったドラッグ要素にクリックで回転する挙動を加えていきます。
e9a9d62886a5cd2d316388c4bf22aaa6.gif
サンプルコード:https://codesandbox.io/s/react-draggble-onclick-jmwz8?file=/src/components/piece.tsx

DOM部分

f83b5ae02e64713cb5844579de5722fa.png
ドラッグのみの時からの主な変更点は3つ。
・onStopイベント追加
・クリック要素にstyleを加え動的に回転を加える
・クリック要素をdivタグでラップ
1つずつ説明していきます。

onStopイベントの追加

ここではonClickイベントではなく、onStopイベントを用いています。
onClickイベントだとドラッグ終了時にも発火してしまいます。それを防ぐためにonClickに状態を持たせることも可能ですが、今回はonStopイベント実行時にて条件分岐を加えることにしました。
条件の判定はuseRefを使っています。

またonStopにてクリック時のアクションを実装する事で、スマホなどのタッチ操作でもクリックと同じアクションを実装できます。
onClickを使った場合、スマホ用にonTouchStartも用いる必要がありそうです。
参考:https://github.com/STRML/react-draggable/issues/49

クリック要素にstyleを加え動的に回転を加えられる

クリックした要素に実際に回転が見た目で加わる様に、style={}で動的にデータを流し込んでいます。
currentRotateにはuseStateにより更新された回転の状態が格納されます。

クリック要素を<div>でラップ

こちらに関しては今回の実装の肝になります!!

回転の実装はクリックによりcurrentRotateが更新され、それをstyleの当て込んで実現します。
しかし、<Draggable>では直下の要素のstyleのtransformを制御してドラッグ機能を実現しています。
<Draggable>直下でのtransform操作はできないので、クリックイベントでstyleを反映させたい要素を<div>でラップする事でtransformを反映させています。

イベント処理部分

bd1c03667195ec7f3e8594e14acb3b04.png
主な変更点は以下の通りです
・onStopイベントの追加
・useRefの追加

onStopイベントの追加

onStopイベントではクリックした時の挙動を加えていきます。
まず前提条件としてonStopはクリック時も、ドラッグ終了時も両方発火してしまうので、 useRefにて条件分岐を行う事でクリック時にのみ処理をあてます。

条件分岐内ではcurrentRotate+90によって、クリックする度に90度の回転を加えています。

useRefの追加

useRefにてドラッグか否かの情報を持ち、それをもとにonStopでの条件分岐を行っています。
onStop時に参照したいだけなので、useStateではなくuseRefを使っています。

参考①:USEREFの使い方がわからん
参考②:React.useRef()を使って無駄なレンダリングを減らそう

まとめ

・ドラッグ終了時の発火を防ぐために、onStopにてクリック時の処理を書く
<Draggable>直下はtransformが制御されている為、動的にスタイリングしたい場合は要素をラップする

上記を踏まえればreact-draggableでもクリックの処理を実現できます。

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

【React入門】学習メモ #1

この記事はReact Advent Calendar 2020 16日目の記事です。
簡単に自己紹介いたしますと、2020/7月にプログラマに転身したひよっこです。この記事を書く経緯としては、自社HPの改修やら、その他フロントエンド開発でReactを触る機会があり、「ちゃんとReactに入門しよう!」と思い、この記事を書くことになりました。

はじめに

React学習の備忘録です。
間違い等ございましたら、ご指摘いただけますと幸いです

JSXとは

公式ドキュメント参照

JavaScript の構文の拡張です。UI がどのような見た目かを記述するために、React とともに JSX を使用することを私たちはお勧めしています。JSX はテンプレート言語を連想させるでしょうが、JavaScript の機能を全て備えたものです。

Reactを扱う上でJSXは必須という訳ではないようですが、
ここでは、「JSXを使うとReactを簡単に表現できるんだなぁ」くらいに思って頂いて大丈夫かと思います。

JSXの特徴や記法

return内に複数の要素を記述しない

App.jsx
render() {
  return(
    <h1>Hello React</h1>
    <h2>Hello Qiita</h2>
    <h3>Hello World</h3>
  );
}

上記のように、return内に複数の要素を記述すると下記のエラーが発生します。

JSX 式には 1 つの親要素が必要です。

上記の問題を解決するためには、下記のように一つの要素にまとめる必要があります。

App.jsx
render() {
  return(
    <div>
      <h1>Hello React</h1>
      <h2>Hello Qiita</h2>
      <h3>Hello World</h3>
    </div>
  );
}

コメント

App.jsx
render() {
  return(
    <div>
      {/* この部分はコメントです */}
    </div>
  );
}

自己終了タグ

JSXでは必ずHTMLの終了タグが必要です。
多くのHTMLタグは終了タグは付いていますが、終了タグを記述する必要がないタグも存在します。

  • img
  • input
  • link
  • area

上記のような終了タグを記述する必要がないタグにはタグの終わりに「/」を付けてあげることで解決します。

App.jsx
render() {
  return(
    <div>
      <img src='https://hogehoge.com/piyopiyo.jpg' />
    </div>
  );
}

クラス名

JSXでは「className='クラス名'」と記述します。

App.jsx
render() {
  return(
    <div>
      <h1 class='msg'>Hello World</h1>  {/* bad */}
      <h1 className='msg'>Hello React!!</h1>  {/* Good */}
    </div>
  );
}

JSXの中にJSを埋め込む

JSXにJSを埋め込む際は中括弧{}で囲む。
中括弧の中ではJavaScriptとして認識されます。

App.jsx
render() {
  const msg = 'Hello React!!';
  return(
    <div>
      { msg }
    </div>
  );
}

タグの属性の値も、同様に中括弧{}を使ってJavaScriptを記述できる

App.jsx
render() {
  const imgUrl = 'https://hogehoge.com/piyopiyo.jpg';
  return(
    <div>
      <img src={ imgUrl } />
    </div>
  );
}

クリックと表示の切り替え

例:ボタンをクリックしたら、画面の表示が切り替わる

上記のようなよく見かけるパターンはイベントstateを使って実装します。
イベント:「何かが起きたときに、処理を実行するように指定」すること。
state:「ユーザーの動きに合わせて変わる値」のこと。

イベントの記述方法

引数が不要の場合

イベント名 = {関数}

引数が必要の場合

イベント名 = {() = 関数(引数)}

onClickイベント

onClickイベントはユーザーが要素をクリックしたときにイベントを発火させる。
下記の例では、ボタンをクリックしたらHello React!!とアラートを出します。

App.jsx
render() {
  return(
    <div>
      <button onClick={() => alert('Hello React!!')}>Click</button>
    </div>
  );
}

state

stateとは、ユーザーの動きに合わせて変わる値のことです。
stateが変更されるタイミングで再レンダリングが行われます。

以下、引用

  • mutable data (可変のデータ)
  • maintained by component (コンポーネントによって保持)
  • can change it (変更可)
  • should be considered private (プライベートであるべき)

State は可変のデータです。State の変更は、仮想DOMとの差分をとり、実際のDOMを更新し、コンポーネントを再描画するための基本的な方法となっています。

state定義

stateはconstructor内でオブジェクトとして定義する
※下記は、クラスコンポーネントの記述

App.jsx
class App extends React.Component {
  consttuctor (props) {
    super(props);
    this.state = {  //ここから
      name: 'masa',
      age: 21
    };  //ここまで
  }
}

state取得

this.stateで取得できる。
定義したstateのnameプロパティの値が欲しければ、this.state.nameで取得できる

App.jsx
render() {
  return(
    <div>
      <h1>名前は、{ this.state.name }です</h1>
      <h1>年齢は、{ this.state.age }です</h1>
    </div>
  );
}

state変更

stateの変更はsetStateで変更できる。

this.setState({ プロパティ名: 変更する値 })

下記では、ボタンを二つ用意し、それぞれのボタンを押下すると表示されている名前と年齢が切り替わる処理になります。

App.jsx
render() {
  return(
    <div>
      <h1>名前は、{ this.state.name }です</h1>
      <h1>年齢は、{ this.state.age }です</h1>
      <button onClick={() => this.setState({ name: 'masahiro', age: 23 })}>masahiro</button>
      <button onClick={() => this.setState({ name: 'masanobu', age: 28 })}>masanobu</button>
    </div>
  );
}

state変更の注意点

Reactでは、「stateの値の直接代入することで値を変更してはいけない」といった決まりがあります。
よって、以下のようなケースはエラーとなります。
stateを変更する際はsetStateを使いましょう。

this.state = { name: 'masahiro' };  //エラー
this.state.name = 'masahiro';  //エラー

this.setState({ name: 'masahiro' })  //Good

カウントアップ機能を作成

順序としては、以下の通り

  • カウントされるstateを定義
  • stateを表示
  • stateを変更するメソッドを定義
  • ボタンにonClickイベントを用意し、上記で定義したメソッドを呼び出す

stateの定義

まず、カウントされるstateを定義します。
カウントの初期値は0です。

App.jsx
constructor(props) {
  super(props);
  this.state = {
    count: 0
  };
}

stateの表示

定義したstateを表示する。

App.jsx
render() {
  return(
    <div>
      { this.state.count }
    </div>
  );
}

stateを変更するメソッドを定義

呼び出したときに、stateを変更してくれるメソッドを定義します。

App.jsx
// カウントアップするメソッド
handleClickCountUp() {
  this.setState({count: this.state.count + 1});
}

クリックイベントでメソッドを呼び出す

onClickイベントと組み合わせて上記で定義したメソッドを呼び出します

App.jsx
<button onClick = {this.handleClickCountUp} >+</button>

成果物

App.jsx
class Sample extends React.Component {
  constructor(props) {
    super(props);
    // stateの定義
    this.state = {
      count: 0
    };
  }

  // カウントアップするメソッド
  handleClickCountUp() {
    this.setState({count: this.state.count + 1});
  }

  render() {
    return(
      <div>
        <h1>
          {/* stateの表示 */}
          { this.state.count }
        </h1>
        {/* クリックイベント */}
        <button onClick = {this.handleClickCountUp} >+</button>
      </div>
    );
  }
}

番外編~ifでカウントを制御する~

上記で作成したカウントアップするプログラムにifでカウントを制御してみる

  • if:countが15以上で
  • this.setState:countを0にする
App.jsx
// カウントアップするメソッド
handleClickCountUp() {
  this.setState({count: this.state.count + 1});

  // ifでカウント制御
  if (this.state.count >= 15) {
    this.setState({count: 0});
  }
}

備考

良ければ、続きの記事も見て頂けますと幸いです。

【React入門】学習メモ #2
【React入門】学習メモ #3

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