20200523のReactに関する記事は9件です。

ReactとRailsを同一サーバーで動かす方法

経緯

Railsの学習を始めるに当たって、 ERBを利用したVIEWでフロント側を構築していたが、
最近はSPAというものが流行っていると小耳に挟んだ。

とりあえずチャレンジ!
ということでReactの学習を並行して進めていたが、、、
これどうやってデプロイするんだ。。。

となってしまいました笑

とりあえず別サーバーで動かすことにしてCORS制約などもなんとなくクリアして無事にRailsとReactのやりとりが可能になった。

しかし、ReactとRailsを利用したアプリ関係の記事を漁っていると、同一サーバーで動かしているものを多数発見!!!
何回かチャレンジしたものの上手く行かず断念。。。

しばらく時間が過ぎ、この土日に絶対動かしてやる!!! と執念で参考記事を探しまくった結果、
Heroku公式のBlogにわかりやすい記事があり、やっと構築ができました笑

同じような人がいたら参考になれば良いなと思い投稿するに至りました!

前提条件

  • Heroku blogの記事から必要最小限の部分だけ引用したものになります
  • Ruby、Rails、Node、npm OR yarnなどの基本的なインストールが済んでいること
  • Railsアプリを作成したことがある
  • Reactアプリをcreate-react-appで作成したことがある
  • Herokuでホスティングをしたことがある(gitでデプロイ)

参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku

Railsアプリの作成

rails new コマンドでrails アプリを作成します。

--apiコマンドでapiモードにするのをお忘れなく

rails new rails_app --api

作成したアプリが起動するか確かめましょう。

cd rails_app
rails s

Reactアプリの作成

Railsアプリのルートディレクトリ上でclientという名前でReactアプリを構築します。

npm create-react-app client

Railsと同じように、Reactが動くか確かめましょう。

cd client
npm start 

このままだと開発中にReactとRailsで別ポートとなるので
package.jsonに設定を追加します。

package.json
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:3001", #ここを追加
  ...
}

この設定によりRails側には、Reactアプリがポート3001で起動しているように認識させます。

React - Rails ローカル編

ローカル環境立ち上げ用のタスクを作成します。

ProcfileにReactとRailsを立ち上げるコマンドを書き、Railsのタスクで呼び出す方法にします。

Procfileはheroku上で利用されるファイルで、プロセスのタイプ:コマンドで記述できます。

<process type>: <command>

今回は、ルートディレクトリにProcfile.devというファイル名で、RailsアプリとReactアプリの起動コマンドを記述します。

Procfile.dev
web: cd client && PORT=3000 npm start
api: PORT=3001 && bundle exec rails s

呼び出しコマンドを簡潔にしたいため、taskにします。

start.rake
namespace :start do
    desc 'Start development server'
    task :dev do
      exec 'heroku local -f Procfile.dev'
    end
end

taskでprocfileの起動コマンドをキックします。

rake start:dev

ReactとRailsがどちらも立ち上がったと思います。

ターミナル上のログでわかりづらい場合はRails側で何かしらJsonを返すようにしておき、React側からfetchできるようにしておくとわかりやすいと思います。

React - Rails Heroku編

メインテーマであるHeroku上へのデプロイです。

まずはルートディレクトリにpackage.jsonファイルを作成します。

RailsのpublicディレクトリにReactの静的ファイルを作成するコマンドを記述します。

package.json
{
    "name": "list-of-ingredients",
    "license": "MIT",
    "engines": {
      "node": "10.15.3",
      "yarn": "1.15.2"
    },
    "scripts": {
      "build": "yarn --cwd client install && yarn --cwd client build",
      "deploy": "cp -a client/build/. public/",
      "heroku-postbuild": "yarn build && yarn deploy"
    }
}

production用のProcfileを作成し、railsの起動コマンドを記述します。

必要に応じてrelease後のmigrateコマンドも記述します。

Procfile
web: bundle exec rails s
release: bin/rake db:migrate



CLIを利用してheroku上のdynoの作成とビルドパックの設定を行います。

アプリの作成
heroku apps:create
ビルドパックの設定
heroku buildpacks:add heroku/nodejs --index 1
heroku buildpacks:add heroku/ruby --index 2

heroku上の準備ができたのでgitでpushします。

git add .
git commit -m "React - Rails"
git push heroku master

アプリを開きます。

heroku open

あとがき

みなさん無事に動きましたでしょうか?

もっと良い方法があれば是非教えていただけるとありがたいです。

もし動かない場合は下記の参考元の記事を読んでみてください!

参考記事:A Rock Solid, Modern Web Stack—Rails 5 API + ActiveAdmin + Create React App on Heroku

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

React + Emotionで作ったアプリがGitHub Actionsでハマった

はじめに

ReactアプリにEmotionを導入して、GitHub ActionsでCI/CD環境構築しようとしたらちょっとハマってしまいました。
ハマったポイントと解決方法を記事にします。

ハマったポイント

まずは私が何をしてどうハマったのか書いていきます。

使おうとしたワークフロー

私はこんなワークフローを使おうとしていました。

name: Firebase Deploy
on:
  push:
    branches:
      - master

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@master
      - name: Install Dependencies
        run: npm install
      - name: Build
        run: npm run build
        env:
          REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }}
          REACT_APP_FIREBASE_AUTH_DOMAIN: ${{ secrets.REACT_APP_FIREBASE_AUTH_DOMAIN }}
          REACT_APP_FIREBASE_PROJECT_ID: ${{ secrets.REACT_APP_FIREBASE_PROJECT_ID }}
          REACT_APP_FIREBASE_APP_ID: ${{ secrets.REACT_APP_FIREBASE_APP_ID }}
          REACT_APP_FIREBASE_CONTINUE_URL: ${{ secrets.REACT_APP_FIREBASE_CONTINUE_URL }}
      - name: Archive Production Artifact
        uses: actions/upload-artifact@master
        with:
          name: build
          path: build
  deploy:
    name: Deploy
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@master
      - name: Download Artifact
        uses: actions/download-artifact@master
        with:
          name: build
          path: build
      - name: Deploy to Firebase
        uses: w9jds/firebase-action@master
        with:
          args: deploy
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

これでいけるやろ!と思っていざプッシュしたところ…

発生したエラー

ワークフローの実行結果を確認すると、ビルドが失敗していました。

Run npm run build

// 中略

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.

Failed to compile.

./src/containers/loginForm.tsx
  Line 2:8:  'React' is defined but never used  @typescript-eslint/no-unused-vars

./src/components/loginForm.tsx
  Line 2:8:  'React' is defined but never used  @typescript-eslint/no-unused-vars

『環境変数CItrueになるからwarningerrorになるよ。大体のCIサーバーで自動的にこうなるよ』と言われました。
具体的なエラーは定義済のReactが未使用というものでした(@typescript-eslint/no-unused-vars)

どうしてこうなった

通常、JSX構文で記述された式はReact.createElement()へコンパイルされるため、Reactをインポートする必要がありますが、Emotionを使う場合はちょっと違います。

Emotionのcss Propを使う場合、JSX Pragmaという特別なコメントをソースファイルのトップに記述する必要があります。

JSX_Pragma
/** @jsx jsx */

このコメントによってReact.createElementの代わりにEmotion独自のjsx関数へコンパイルされるようになります。
これでcssという便利なpropが使えるようになり、JSの中でCSSを記述できるようになるわけですが、Reactが使われなくなります。なのでこのままだと@typescript-eslint/no-unused-varsに抵触します。これを何とかするために.eslintrc.jsでrulesを追加します。

module.exports = {
  // 中略
  rules: {
  // 中略
    "@typescript-eslint/no-unused-vars": [
      "error",
      {
        varsIgnorePattern: "[Rr]eact",
      },
    ],
    // 中略
  },
  // 中略
};

Emotionを使う場合はやや手間をかけてあげないといけないのですが、今回はこれが原因でエラーが発生したわけです。

解消方法

解消するにあたって↓のIssueを参考に、ビルドのステップを実行する際の環境変数CIfalseに設定しました。
Stop Treating warnings as errors because process.env.CI = true. #3657

修正したワークフローがこちら

name: Firebase Deploy
on:
  push:
    branches:
      - master

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@master
      - name: Install Dependencies
        run: npm install
      - name: Build
        run: npm run build
        env:
          CI: false
          REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }}
          REACT_APP_FIREBASE_AUTH_DOMAIN: ${{ secrets.REACT_APP_FIREBASE_AUTH_DOMAIN }}
          REACT_APP_FIREBASE_PROJECT_ID: ${{ secrets.REACT_APP_FIREBASE_PROJECT_ID }}
          REACT_APP_FIREBASE_APP_ID: ${{ secrets.REACT_APP_FIREBASE_APP_ID }}
          REACT_APP_FIREBASE_CONTINUE_URL: ${{ secrets.REACT_APP_FIREBASE_CONTINUE_URL }}
      - name: Archive Production Artifact
        uses: actions/upload-artifact@master
        with:
          name: build
          path: build
  deploy:
    name: Deploy
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@master
      - name: Download Artifact
        uses: actions/download-artifact@master
        with:
          name: build
          path: build
      - name: Deploy to Firebase
        uses: w9jds/firebase-action@master
        with:
          args: deploy
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

これで無事エラーが解消されました:grinning:

参考

The css Prop #jsx-pragma

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

Reactでfullcalenderを使ってみたのでまとめてみる

はじめに

ReactでFullcalenderを使って、カレンダーアプリっぽいことをする機会があったので、使い方など書いていきます。

使ったもの

  • react(v16.13.1)
  • react-datepicker(v2.16.0)
  • @fullcalendar/daygrid(v4.4.0)
  • @fullcalendar/interaction(v4.4.0)
  • @fullcalendar/react(v4.4.0)
  • @fullcalendar/timegrid(v4.4.0)

作りたいもの

以下機能を入れる
* スケジュールの保存
* スケジュールの変更
* スケジュールの削除
* カレンダーを範囲選択してその時間が初期入力された入力フォームを出す
* イベントをクリックでそのイベントの内容が初期入力された変更フォームを出す

デモはこちら
スマホ対応してません・・・

2020-05-23_18h15_21.png
2020-05-23_18h16_08.png
2020-05-23_18h16_22.png

プロジェクト作成

create-react-appでReactのプロジェクトを作成して、上記のとおり必要なパッケージをインストールします。(react以外)

create-react-app app
npm install --save 

カレンダー部分を作る

インポート

Fullcalenderに必要なパッケージをインポートします。

App.js
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import "@fullcalendar/core/main.css";
import "@fullcalendar/daygrid/main.css";
import "@fullcalendar/timegrid/main.css";

カレンダーを実装

カレンダーを表示したいところに以下置いていく。
他にもいろいろ設定できたりしますので、公式ページを参照してください。

App.js
<FullCalendar
  locale="ja" // 日本語
  defaultView="timeGridWeek" // 基本UI
  slotDuration="00:30:00" // 表示する時間軸の最小値
  selectable={true} // 日付選択可能
  allDaySlot={false} // alldayの表示設定
  titleFormat={{
    year: "numeric",
    month: "short",
    day: "numeric",
  }} // タイトルに年月日を表示
  header={{
    left: "prev,next,today",
    center: "title",
    right: "dayGridMonth,timeGridWeek",
  }}
  businessHours={{
    daysOfWeek: [1, 2, 3, 4, 5],
    startTime: "0:00",
    endTime: "24:00",
  }}
  plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
  ref={this.ref}
  weekends={true} // 週末表示
  events={this.myEvents} // 起動時に登録するイベント
  select={this.handleSelect} // カレンダー範囲選択時
  eventClick={this.handleClick} // イベントクリック時
/>

this.myEventsはカレンダーに表示するイベントの配列で、titlestartendがあればだいたいいい感じに表示されます。
endがない場合は1時間と範囲になります。

ここではmemoを追加して予定に関するメモを残せるようにしています。

App.js
    this.myEvents = [
      {
        id: 0,
        title: "event 1",
        start: "2020-05-22 10:00:00",
        end: "2020-05-22 11:00:00",
        memo: "memo1",
      },
      {
        id: 1,
        title: "event 2",
        start: "2020-05-23 10:00:00",
        end: "2020-05-23 11:00:00",
        memo: "memo2",
      },
    ];

入力・変更フォームを実装する

this.state.isChangeで入力or変更の切り替えをしています。

  • カレンダーの範囲選択時→入力
  • イベントクリック時→変更

this.state.formInviewでフォームの表示・非表示の切り替えをしています。

App.js
  renderForm() {
    return (
      <div
        className={
          this.state.formInview ? "container__form inview" : "container__form"
        }
      >
        <form>
          {this.state.isChange === false ? (
            <div className="container__form__header">予定を入力</div>
          ) : (
            <div className="container__form__header">予定を変更</div>
          )}
          <div>{this.renderTitle()}</div>
          <div>{this.renderStartTime()}</div>
          <div>{this.renderEndTime()}</div>
          <div>{this.renderMemo()}</div>
          <div>{this.renderBtn()}</div>
        </form>
      </div>
    );
  }

フォームの中身は以下の通りになります。

  • タイトル
  • 開始日時
  • 終了日時
  • メモ
  • キャンセルor削除ボタン
  • 保存or変更ボタン

フォームの内容に変更があった場合に、stateで管理しているものをonChangeで更新しています。

日時部分の実装にはDatePicker を使用しています。開始日時が終了日時より遅くなったときに終了日時を開始日時に合わせるみたいな処理をいれたほうがいいですが、とりあえずこんな感じで。。

App.js
  renderTitle() {
    return (
      <React.Fragment>
        <p className="container__form__label">タイトル</p>
        <input
          className="container__form__title"
          type="text"
          value={this.state.inputTitle}
          onChange={(e) => {
            this.setState({ inputTitle: e.target.value });

            if (e.target.value === "") {
              this.setState({ isInputTitle: false });
            } else {
              this.setState({ isInputTitle: true });
            }
          }}
        />
      </React.Fragment>
    );
  }
  renderMemo() {
    return (
      <React.Fragment>
        <p className="container__form__label">メモ</p>
        <textarea
          className="container__form__memo"
          rows="3"
          value={this.state.inputMemo}
          onChange={(e) => {
            this.setState({ inputMemo: e.target.value });
          }}
        />
      </React.Fragment>
    );
  }
  renderStartTime() {
    return (
      <React.Fragment>
        <p className="container__form__label">開始日時</p>
        <DatePicker
          className="container__form__datetime"
          locale="ja"
          dateFormat="yyyy/MM/d HH:mm"
          selected={this.state.inputStart}
          showTimeSelect
          timeFormat="HH:mm"
          timeIntervals={10}
          todayButton="today"
          onChange={(time) => {
            this.setState({ inputStart: time });
          }}
        />
      </React.Fragment>
    );
  }
  renderEndTime() {
    return (
      <React.Fragment>
        <p className="container__form__label">終了日時</p>
        <DatePicker
          className="container__form__datetime"
          locale="ja"
          dateFormat="yyyy/MM/d HH:mm"
          selected={this.state.inputEnd}
          showTimeSelect
          timeFormat="HH:mm"
          timeIntervals={10}
          todayButton="today"
          onChange={(time) => {
            this.setState({ inputEnd: time });
          }}
        />
      </React.Fragment>
    );
  }
  renderBtn() {
    return (
      <div>
        {this.state.isChange === false ? (
          <div>
            <input
              className="container__form__btn_cancel"
              type="button"
              value="キャンセル"
              onClick={() => {
                this.setState({ formInview: false });
              }}
            />
            <input
              className="container__form__btn_save"
              type="button"
              value="保存"
              disabled={!this.state.isInputTitle}
              onClick={this.onAddEvent}
            />
          </div>
        ) : (
          <div>
            <input
              className="container__form__btn_delete"
              type="button"
              value="削除"
              onClick={this.onDeleteEvent}
            />
            <input
              className="container__form__btn_save"
              type="button"
              value="変更"
              onClick={this.onChangeEvent}
            />
          </div>
        )}
      </div>
    );
  }

カレンダー範囲選択時の処理を実装する

カレンダーの範囲選択時に指定した時間で入力フォームを初期表示するために、stateで管理しているタイトル・開始日時・終了日時・メモの値を更新します。

App.js
  handleSelect = (selectInfo) => {
    let start = new Date(selectInfo.start);
    let end = new Date(selectInfo.end);
    start.setHours(start.getHours());
    end.setHours(end.getHours());

    this.setState({ inputTitle: "" });
    this.setState({ inputMemo: "" });
    this.setState({ isInputTitle: false });
    this.setState({ inputStart: start });
    this.setState({ inputEnd: end });
    this.setState({ isChange: false });
    this.setState({ formInview: true });
  };

イベントクリック時処理を実装する

イベントクリック時に保存されている値で変更フォームを初期表示するために、stateで管理しているタイトル・開始日時・終了日時・メモの値を更新します。

App.js
  handleClick = (info) => {
    this.selEventID = info.event.id;
    const selEvent = this.myEvents[info.event.id];
    const title = selEvent.title;
    const memo = selEvent.memo;
    const start = new Date(selEvent.start);
    const end = new Date(selEvent.end);

    this.setState({ inputTitle: title });
    this.setState({ inputMemo: memo });
    this.setState({ isInputTitle: true });
    this.setState({ inputStart: start });
    this.setState({ inputEnd: end });
    this.setState({ isChange: true });
    this.setState({ formInview: true });
  };

追加時の処理を実装する

登録したいイベントをeventに一旦いれていき、addEventで登録します。

App.js
  onAddEvent() {
    const starttime = this.changeDateToString(this.state.inputStart);
    const endtime = this.changeDateToString(this.state.inputEnd);

    if (starttime >= endtime) {
      alert("開始時間と終了時間を確認してください。");
      return;
    }
    const event = {
      title: this.state.inputTitle,
      memo: this.state.inputMemo,
      start: starttime,
      end: endtime,
    };
    if (this.addEvent(event) === true) {
      window.alert("設定しました");
      this.setState({ formInview: false });
    }
  }

this.myEventsにpushして、表示も更新するために、this.ref.current.getApi().addEvent(ev);を呼びます。

App.js
  addEvent = (ev) => {
    ev.id = this.getID();
    this.myEvents.push(ev);
    this.ref.current.getApi().addEvent(ev);
    return true;
  };

いい感じにidを取得できる関数たち

App.js
  sortEventID = (a, b) => {
    return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
  };
  getID = () => {
    this.myEvents.sort(this.sortEventID);
    let i;
    for (i = 0; i < this.myEvents.length; i++) {
      if (this.myEvents[i].id !== i) {
        break;
      }
    }
    return i;
  };

いい感じに時間表示を変換する関数たち

App.js
  changeDateToString(dt) {
    const year = dt.getFullYear();
    const month = this.getdoubleDigestNumer(dt.getMonth() + 1);
    const date = this.getdoubleDigestNumer(dt.getDate());
    const hour = this.getdoubleDigestNumer(dt.getHours());
    const minutes = this.getdoubleDigestNumer(dt.getMinutes());

    const retDate = `${year}-${month}-${date} ${hour}:${minutes}:00`;
    return retDate;
  }
  getdoubleDigestNumer(number) {
    return ("0" + number).slice(-2);
  }

変更時の処理を実装する

変更したいイベントをeventに一旦いれていき、changeEventで変更します。

App.js
  onChangeEvent(values) {
    if (window.confirm("変更しますか?")) {
      const starttime = this.changeDateToString(this.state.inputStart);
      const endtime = this.changeDateToString(this.state.inputEnd);

      if (starttime >= endtime) {
        alert("開始時間と終了時間を確認してください。");
        return;
      }

      const event = {
        title: this.state.inputTitle,
        memo: this.state.inputMemo,
        start: starttime,
        end: endtime,
        id: this.selEventID,
      };
      if (this.changeEvent(event) === true) {
        window.alert("イベントを変更しました。");
        this.setState({ formInview: false });
      }
    } else {
      return;
    }
  }

this.myEventsの指定したidの内容を書き換えて、一旦表示を消してから再登録します。

App.js
  changeEvent = (ev) => {
    this.myEvents[ev.id].title = ev.title;
    this.myEvents[ev.id].memo = ev.memo;
    this.myEvents[ev.id].start = ev.start;
    this.myEvents[ev.id].end = ev.end;

    this.ref.current.getApi().getEventById(ev.id).remove();
    this.ref.current.getApi().addEvent(this.myEvents[ev.id]);

    return true;
  };

削除時の処理を実装する

削除したいイベントにisDel = trueを設定しています。一応DB連携した時に何を削除したかをわかるようにthis.myEventsから削除しないようにしています。

App.js
  onDeleteEvent() {
    if (window.confirm("削除しますか?")) {
      if (this.selEventID < this.EVENT_SEL_NON) {
        let EventID = this.selEventID;
        let delevent = this.ref.current.getApi().getEventById(EventID);
        delevent.remove();
        this.selEventID = this.EVENT_SEL_NON;
        this.myEvents[EventID].isDel = true;
      }
      this.setState({ formInview: false });
      alert("イベントを削除しました。");
    } else {
      return;
    }
  }

まとめ

これでだいたいできたと思います。
入力フォーム表示時に他をクリックすると消えたり、スタイル調整とかは割愛します。
DB連携とログイン機能を入れるだけで簡単にそれなりのカレンダーアプリができてしまいますので結構いいですね!

ソース全文はGitHubにありますので、よろしければ確認してみてください。

間違っていたり、こうしたほうがいいよとかありましたら、コメントお願いします!!

参考にしたもの

FullCalendarの使い方
公式ページ

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

es-lintでソースの自動整形を適用させる方法(vs code) 自分用メモ

教えてもらった内容のメモなのでこれだけで動作するかはわかりません、、
あとで検証して更新するのでゆるしてください:sob:

es-lintの公式でだいたいは書いてある?

プラグインの導入

今回はes-lintprettierを併用して行きたいと思います。

下記のコマンドを実行します。
以下の2つはes-lintとprettierのフォーマットを競合させないように必要らしい

  • eslint-config-prettier
  • eslint-plugin-prettier
$ yarn add eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-react prettier -D

ソースコード側の設定

.vscodeディレクトリ内にextensions.jsonを用意します。

extensions.json
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
  ]
}

.vscodeディレクトリ内settings.jsonに以下を追記します。

settings.json
{
  "eslint.packageManager": "yarn",
  "eslint.workingDirectories": [
    { "directory": "./app", "changeProcessCWD": true }
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
}

eslintrc.jsと.prettierrcの設定は下記の通り、、
最低限の条件で適用しているのでその他のルールを追加したい場合はextendsに追記していくといいそうです。

rulesにはignoreする条件等を書きますがこちらは最低限にしないとlintの意味がなくなってしまうのでなるべく追加しないようにしたい。

eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/eslint-recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier/@typescript-eslint',
    'prettier/react',
  ],
  plugins: ['@typescript-eslint', 'react', 'prettier'],
  env: { node: true, es6: true },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  rules: {
    'react/prop-types': 'off',
    '@typescript-eslint/explicit-function-return-type': 0,
    'no-restricted-imports': [
      'error',
      {
        patterns: ['../'],
      },
    ],
  },
  settings: {
    react: {
      version: 'detect',
    },
  },
};
.prettierrc
{
  "eslint.packageManager": "yarn",
  "eslint.workingDirectories": [
    { "directory": "./app", "changeProcessCWD": true }
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
}

以上でできるはず...!

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

react-big-calendar

公式

http://jquense.github.io/react-big-calendar/examples/index.html

Is 何

以下のようなイベントカレンダーを作れる。
fullcalendarに影響を受けている。つまりこれの後発。

image.png

image.png

work 週

image.png

image.png

一覧

image.png

アーキテクチャ

Calendar

カレンダー本体。
Calendarタグのプロパティで諸々の設定を行う。

Event

カレンダー内の予定をEventと呼んでいる。
以下の定義をデフォルトとしている。
デフォルトと書いたのは、APIからアクセスする際のキーを上書き指定できるため。
これにより、Eventのプロパティ名も自在(合わせてアクセスキーを指定する必要はある)。

Event {
  title: string,
  start: Date,
  end: Date,
  allDay?: boolean
  resource?: any,
}

Resource

スケジュールを持つ主体。ユーザーだったり、施設だったり。
公式Documentに説明はない。が、触発されたと記載があるfullcalendar.ioのドキュメントのResourceを読むと概念的に整合する。(怒
EventにはResourceを紐付けることができる。
リソース特定方法は、resourceAccessorがある。
デフォルトはDocumentのとおり、resourceId。
EventにresourceIdプロパティを加えればすぐ使える。

Resourceを定義すると、この画面のように上部にResource.titleが表示される。

image.png

Localization and Date Formatting

日付のフォーマットとローカライゼーションが外部化されている。
momentが利用できる。

カスタマイズ性

Documentにも説明はあるが、Calendarのプロパティ「component」にカスタマイズした描画コンポーネント(jsx)を渡せる。

  • 渡し方
let components = {
  event: MyEvent, // used by each view (Month, Day, Week)
  toolbar: MyToolbar,
  agenda: {
       event: MyAgendaEvent // with the agenda view use a different component to render events
  }
}
<Calendar components={components} />
  • 再定義可能なものの全量(@typesから抜いた)

どこまでカスタマイズできるかは見ていない。

export interface Components<TEvent extends object = Event> {
    event?: React.ComponentType<EventProps<TEvent>>;
    eventWrapper?: React.ComponentType<EventWrapperProps<TEvent>>;
    eventContainerWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    dateCellWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    timeSlotWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    timeGutterHeader?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    timeGutterWrapper?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    toolbar?: React.ComponentType<ToolbarProps>;
    agenda?: {
        date?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
        time?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
        event?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    };
    day?: {
        header?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
        event?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    };
    week?: {
        header?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
        event?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    };
    month?: {
        header?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
        dateHeader?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
        event?: React.SFC | React.Component | React.ComponentClass | JSX.Element;
    };
    /**
     * component used as a header for each column in the TimeGridHeader
     */
    header?: React.ComponentType<HeaderProps>;
    resourceHeader?: React.ComponentType<ResourceHeaderProps>;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gatsbyサイトの動的ページにコメント欄機能を:Desqus設置

Gatsbyサイトの動的ページにコメント欄機能を:Desqus設置

簡単ではあるが、少し手間取ったので備忘録。

Disqus登録

https://disqus.com/
image.png

サインアップしてログインするとこのページに。
I want install disqus on my siteをえらぶ。
image.png

サイト名を入力してカテゴリを選ぶだけで良いようだ。
image.png

Got it!...のボタンをクリック
image.png

管理者としてのホームコンソール
image.png

複数サイトを持ってる場合はメニューで目的のサイトをクリックし
image.png

右端のEdit Settingをクリックすると、設定ページに飛ぶ。
image.png

いちおう、言語をJapaneseにして保存。
image.png
image.png

Disqusインストールと設定・設置

gatsby-plugin-disqusdisqus-reactとある。両方試したがdisqus-reactの方が安定して素直に設置できたのでそちらを。

インストール
$ yarn add disqus-react
global-config.js

いじらない。

記事表示ファイルarticle.jsの編集。

gatsby-plugin-disqusをインポート

// templates/article.js
import { DiscussionEmbed } from "disqus-react";

そして/* コメント欄機能Disqusの設定 */ のところを追加。

// templates/article.js

import React from "react"
import { graphql } from "gatsby"

import Layout from "../components/layout"
import SEO from "../components/seo"
import { Container, Row, Col, Breadcrumb} from 'react-bootstrap'

import { useLocation } from "@reach/router"
import { DiscussionEmbed } from "disqus-react";

const Articlearticle = props => {

 const article = props.data.microcmsArticles // ㊟allMicrocmsArticleでない

 /* コメント欄機能Disqusの設定 */
 const slug = useLocation()
 const title = article.title
 const disqusShortname = "bij-xxxx-x";
 const disqusConfig = {
  config: { identifier: slug, title },
}
・・・略・・・
  • slugはuseLocation()でURLパスを持ってきて格納。
  • titleはmicrocmsArticlesのGraphQLから持ってきて格納。
  • disqusShortnameはDisqusコンソールに表示されている。 image.png
DiscussionEmbed埋め込み

次のように、レンダーしたいところにshortnameとconfigのプロパティをつけて埋め込む。

<DiscussionEmbed shortname={disqusShortname} config={disqusConfig} />

コード全容

いちおうarticle.jsのコード全文

// templates/article.js

import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SEO from "../components/seo"
import { Container, Row, Col, Breadcrumb} from 'react-bootstrap'
import { useLocation } from "@reach/router"
import { DiscussionEmbed } from "disqus-react";

const Articlearticle = props => {

 const article = props.data.microcmsArticles // ㊟allMicrocmsArticleでない


 /* コメント欄機能Disqusの設定 */
 const slug = useLocation()
 const title = article.title
 const disqusShortname = "bij-site-2";
 const disqusConfig = {
  config: { identifier: slug, title },
}


 return (
   <Layout>
     <Container fluid="md">
     <SEO title={article.title} 
        description={sumarrize(article.body)}
        image={article.pict.url}  
        lang="ja"
    />
     <div>
       <h1 style={{ fontSize: `1.25rem`}}>{article.title}</h1>
       <span style={{ fontSize: `1.1rem`}}
                dangerouslySetInnerHTML={{
                  __html: `${article.title_origin}`,
                }}
       >
       </span>
       <Row>
         <Col md={8}>
         <span style={{ fontSize: `0.9rem`, color: `gray` }}>著者:{article.writer.name}</span>
         </Col>
         <Col md={4}>
         <span style={{ fontSize: `0.9rem`, color: `gray` }}>投稿:{article.date}</span>
         </Col>
       </Row>     
       <br />
       <p
         dangerouslySetInnerHTML={{
           __html: `${article.body}`,
         }}
       ></p>
       <br />
       <span>著者:{article.writer.name}</span>
       <br />
       <img src={article.writer.image.url} width={160} alt={article.writer.name} />
       <p
         dangerouslySetInnerHTML={{
           __html: `${article.writer.profile}`,
         }}
       ></p>
     </div>
     <DiscussionEmbed shortname={disqusShortname} config={disqusConfig} />
     </Container>
   </Layout>
 )
}

export default Articlearticle

export const query = graphql`
 query($id: String!) {
   microcmsArticles(id: { eq: $id }) {
     title
     title_origin
     date
     body
     pict {
       url
     }
     body
     category {
       name
     }
     writer {
       name
       profile
       image {
         url
       }
     }
   }
 }
`

/* <SEO description= />用に記事本文を120字以内に収める関数 */
let striptags = require('striptags');
function sumarrize(html) {
  const metaDescription = striptags(html).replace(/\r?\n/g, '').trim();
  return metaDescription.length <= 120
    ? metaDescription
    : metaDescription.slice(0, 120) + '...';
}


できた。
image.png

ページネーションなどもできるようだがそれは追々。

Disqusコンソール

コメントはDisqusコンソールで確認して削除したりできる。

https://bij-xxxx.disqus.com/admin/moderate/approved
image.png

無料トライアル1か月が終了したら?

Nakamuさんにコメント欄で聞いたところ、そのまま使えてるようです。

参考:

Adding Comments
Add Disqus comments to a Gatsby blog
Gatsby Plugin Disqus
What platform is your site on?
Universal Embed Code
GatsbyJSで構築したブログにDISQUSのコメントフォームを設置する方法

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

ReasonReact から Recoil を使う

ReasonReact から Recoil を使う

Facebook が新しい React 用の状態管理ライブラリを公開しました。 Recoil という名前です(ググラビリティ〜)。
というわけで、これを ReasonReact から使ってみます。

利用するライブラリのご紹介

知っている方は飛ばして結構です。

Recoil

Recoil

Facebook 発の React 用状態管理ライブラリ。
状態管理を、 Atom という状態と Selector という変換処理(純粋関数……って書いてあるんですけど普通に副作用を含める事を想定しているように見えますね?)で行います。この、 Atom から Selector を通してコンポーネントまでデータを持ってくる一連の流れを「データフローグラフ」と表現しているようです。
状態の名前が Atom というのは何となく Clojure を思い出しますね。

Atom は React Hooks の useState に似ていますね。ただし、 Recoil の仕組みを利用して同じ状態を複数のコンポーネントから利用できます。
Selector は、 SQL のビューみたいな感じですね。更新処理を持たせる事ができるのも面白い。

Reason

Reason

皆さんご存知、 Facebook が提供している OCaml ベースの AltJS ですね。 BuckleScript という、見た目そのまま OCaml の AltJS を、文法のみ JavaScript 風味にしたもので、 AST レベルでは完全に OCaml なのが特徴です。
強い型安全性、高いインターオペラビリティ、 JavaScript に変換された後の効率の良さが特徴とされています。特に JavaScript との連携の容易さは強く意識されており、コンパイルされた後の JavaScript コードの可読性や、利用性の高さに注意を払って開発されている点が強みです。

最近の改善で、 OCaml のレコードを JavaScript のオブジェクトにマッピングするようになり、 JavaScript との連携はより強力になりました。

// 型定義
type user = {
  id: int,
  name: string,
};

let user1 = {id: 42, name: "John"};
let user2 = {id: 43, name: "Alice"};

// 関数定義
// 関数ヘッドでのパターンマッチと分解が可能。
// 文字列補間も利用できる(ちょっと冗長な書き方が必要だが……)。
let displayUser = ({id, name}) => {j|[$(id)] $(name)|j};

このコードが次のように変換されます(コメント部分は筆者。全体的に、ちょっと省略して順番をいじっています)。

// 型定義はコンパイル後には消えている。 OCaml は実行時に型情報を持たない。

// OCaml のレコードの定義が JavaScript のオブジェクトの定義で表現されている!
var user1 = {
  id: 42,
  name: "John"
};

var user2 = {
  id: 43,
  name: "Alice"
};

// ES5 互換の JavaScript を吐き出すので、アロー演算子や分割代入や文字列補間は利用されない。
function displayUser(param) {
  return "[" + (String(param.id) + ("] " + (String(param.name) + "")));
}

すごい。

ReasonReact

ReasonReact

React の Reason 向けポーティングです。
同じ Facebook だけあってか React の中の人が開発に関わるなどしており、コミュニティもなかなか活発で、見ていて楽しいです。
関数型言語と React との相性の良さを味わってください。

Recoil を ResonReact から利用する

ではその Recoil を、 ReasonReact から使ってみます。

Reason で JavaScript のライブラリを利用するのは非常に簡単なのですが、静的型付き言語なので、型定義を書いてやる必要があります。
実は Recoil の型定義については、既に @bloodyowl さんという方が書いていらっしゃるので、これを利用するのが早いでしょう。

reason-recoil

が、今回は型定義を書く練習も兼ねて、必要な部分のみを自分で実装してみる事にします。
新しいライブラリに慣れるためにそのライブラリの型定義を書いてみるというのは、用意された関数に知悉するためにも良い行いかと思います。

Recoil の型定義

書いていきましょう。
Reason から JavaScript の関数や値を呼び出す時の型定義については、ここを参考にします。

Recoil.re という名前のファイルを作り、そこに定義を書いていきます。

Recoil の型

まず、状態の型を作ります。この状態というのは、

  • atom
  • 書き込み可能な selector

のどちらかです。

本来は、 selector が書き込み可能か不可能かをきっちりと型で表現するべきではありますが、今回は簡便さを優先してそこを無視します。
以下のような定義になります。

type t('a)

型引数を一つだけ取る t という型を定義します。その型が実際どういうものなのかは、一切書く必要がありません。

RecoilRoot

次に、 RecoilRoot コンポーネントを定義します。

module Root = {
  [@react.component] [@bs.module "recoil"] external make: (~children: React.element) => React.element = "RecoilRoot";
};

外部ライブラリのコンポーネント導入はこの辺りを参考にしました。
ここでも、本来は initializeState という prop を取るのですが、今回は省略しています。

atom

そして、 atom を定義します。

type atomOption('a) = {
  key: string,
  default: 'a,
};
[@bs.module "recoil"] external atom: atomOption('a) => t('a) = "atom";
let atom = (~key, ~default) => atom({key, default});

atom の定義はこの辺りを参考にしました。
同名の関数を定義して、この二つのフィールドはラベル付き引数として取るようにしています。
(別に直接オブジェクトを引数に取っても良いのですが、ラベル付き引数の方がより「らしく」見えるのでこう書きました。)

selector

その次に selector を定義します。

type getter = {get: 'a. t('a) => 'a};
type selectorOption('a) = {
  key: string,
  get: getter => 'a,
};
[@bs.module "recoil"] external selector: selectorOption('a) => t('a) = "selector";
let selector = (~key, ~get) => selector({key, get});

今回は書込み可能な selector は無視して、読み取り専用の selector の型定義のみを作ります。
同じく定義はこの辺りを参考にしています。
getter の型定義の 'a. は多相型注釈です。 この getter は利用される時に、同じ関数内で 'a の型が異なる場合が考えられる為、特定の型に固定しておく事ができません。だから、多相型注釈を使う必要があったんですね。
この辺りは @bloodyowl さんのコードを参考にしました。

Hooks

最後に Hooks を定義しましょう。

[@bs.module "recoil"] external useRecoilState: t('a) => ('a, ('a => 'a) => unit) = "useRecoilState";
[@bs.module "recoil"] external useRecoilValue: t('a) => 'a = "useRecoilValue";
type setter('a) = 'a => 'a;
[@bs.module "recoil"] external useSetRecoilState: t('a) => setter('a) = "useSetRecoilState";

この辺りは特に説明することは無いですね。
公式サイトの Intro to External に書いてある通りにするだけです。  

実装

では、 Recoil を利用する実装を書いていきましょう。
今回は、 Recoil の公式サイトに載っている小さなデモを実装してみます。
テキストボックスに文字列を入力すると、それと同じ文字列と、文字数とを表示してくれるというやつですね。

atom

atom 関数を使って状態を作りましょう。

let textState = Recoil.atom(~key="textState", ~default="");

特に型注釈はありませんが、きちんと Recoil.t(string) 型であると推論されます。

selector

同じく、 selector 関数で selector を作ります。

let charCountState = Recoil.selector(
  ~key="charCountState",
  ~get=({get}) => get(textState) |> String.length
);

こちらも特に型注釈無く、 Recoil.t(int) 型であると推論されます。

コンポーネント

では、コンポーネントを作って上の Recoil の atom や selector を使ってみましょう。
まず、文字列を入力するコンポーネントです。

module Input = {
  [@react.component]
  let make = () => {
    let (textValue, setNameValue) = Recoil.useRecoilState(textState);
    let onChange = React.useCallback(e => setNameValue(e->ReactEvent.Form.target##value));
    <div> <input value=textValue onChange /> </div>;
  };
};

次に、入力された文字列と、その文字数とを表示するコンポーネント。

module Show = {
  [@react.component]
  let make = () => {
    let textValue = Recoil.useRecoilValue(textState);
    let charCountValue = Recoil.useRecoilValue(charCountState);
    <> <div> {j|Echo: $(textValue)|j}->React.string </div> <div> {j|Character Count: $(charCountValue)|j}->React.string </div> </>;
  };
};

これらのコンポーネントを同一の RecoilRoot コンポーネントの中に入れる事で、二つの異なるコンポーネントの間で状態を共有する事ができます。

[@react.component]
let make = () => <Recoil.Root> <Input /> <Show /> </Recoil.Root>;

これで、 ReasonReact から Recoil を使う事ができました。
コードの全容はここに置いてあります。

感想と注釈

後半、ほとんど説明が無かった事に気付かれたかと思います。
何故かと言うと、簡単で、 説明するべき事が殆ど無い からです。
細かい文法の差異を除けば、 JavaScript とほぼ同じような書き方で使う事ができます。何を解説しろというのか、という話ですね。

このように、 Recoil は ReasonReact から全く違和感なく利用できます。

ただし、今回解説したのは本当に Recoil の上辺の部分だという点には注意してください。
Recoil には非同期処理を扱う為の機能なども含まれていますが、今回はそれに関しては全く触れていません。この辺りを ReasonReact で利用するとなると、 async/await を用いて書かれた Recoil のコードとはかなり異なった見た目になる事は間違いないでしょう。機能としては申し分なく利用できるはずですが。

また、 Recoil も開発中のライブラリなので、 API は今後も変更されていくかもしれません。大規模に利用するのはまだ得策では無いかもしれませんね。

とはいえ、 Reason も Recoil もとても面白いので、この記事を読んで興味を持てるのであれば、是非とも触ってみてください。

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

Reactチュートリアルを噛み砕く

できる限り丁寧にReactチュートリアルの内容をまとめました。
なお、こちらの記事は完全なるHYOP(独りよがりアウトプット)であるということを、はじめにご承知いただきたく存じます。

*本記事ではcloud9を用いて開発を進めていきます*
※一部、オリジナルの内容を変更しています。

初期設定

まずはnodeの確認

ec2-user:~/environment(master)$ node -v
v14.2.0

react-boardgameという名前でプロジェクトを作成します。

ec2-user:~/environment(master)$ npx create-react-app react-boardgame

作成したプロジェクト内の、srcディレクトリにあるソースファイルを削除します。

ec2-user:~/environment (master) $ cd react-boardgame/
ec2-user:~/environment/react-boardgame (master) $ cd src
ec2-user:~/environment/react-boardgame/src (master) $ rm -f *
ec2-user:~/environment/react-boardgame/src (master) $ cd ..
ec2-user:~/environment/react-boardgame (master) $ 

srcディレクトリ配下に、新たに index.css、index.js を追加します。

ec2-user:~/environment/react-boardgame (master) $ touch src/index.css src/index.js

それぞれのファイルに記述するコードを下に記します。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);
index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

下記のコマンドを実行し、サーバーを立ち上げます。

ec2-user:~/environment/react-boardgame (master)$ npm start

エディタ上部にあるPreviewタブからPreviewRunningApplicationを選択し、
スクリーンショット 2020-05-21 13.45.06.png
このような画面が表示されればOK。

ところで、Reactとは何か。

React はユーザインターフェイスを構築するための、宣言型で効率的で柔軟な JavaScript ライブラリです。複雑な UI を、「コンポーネント」と呼ばれる小さく独立した部品から組み立てることができます。 
(公式ドキュメントから引用)

とのことです。

class コンポーネント名 extends React.Component

から始まる部分がコンポーネントに当たります。
今回のコードで言うとSquare Board Gameの三つです。

データの流れとしては
【親】Game〜>Board〜>Square【子】
となり、この過程で、Propsというパラメーターが親コンポーネントから子コンポーネントに受け渡されています。

抽象的な例えかもしれませんが、
Squareコンポーネントは、二次元的です。「一つ一つのマスにどんな値を示すか」にフォーカスしています。
Boardコンポーネントは、三次元的です。奥行きを持って盤面の全体を俯瞰し、Squareに対して表示するべきデータを渡します。
Gameコンポーネントは、そこに時間軸が加わった四次元的な存在です。ゲーム全体の情報を記憶し、それぞれの着手に対応した盤面の情報をBoardに渡します。

文章だけではアレなので、実際に動かしていきましょう。

データを Props 経由で渡す

BoardコンポーネントのrenderSquareメソッド内で、Squareコンポーネントにvalueという名前のpropsを渡します。

index.js
class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }
}

そして、Squareコンポーネントのrenderメソッド内で、渡された値を受け取るため{/* TODO */}を{this.props.value} に書き換えます。

index.js
class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

スクリーンショット 2020-05-21 15.17.12.png
数字が表示されればOKです。

インタラクティブなコンポーネントを作る

Squareコンポーネントをクリックした際に、'X'が表示されるように書き換えていきます。
それに際して、Squareコンポーネントに自分がクリックされたことを "覚えさせる" 必要がありますね。
コンポーネントが何かを「覚える」ためには、state というものを使います。

index.js
class Square extends React.Component {

 constructor(props){
      super(props);
      this.state={
         value: null,                               
      };
  }

  render() {
    return (
      <button 
          className="square" 
         onClick={() => this.setState({value: 'X'})} 
      >
       {this.state.value}                            
      </button>
    );
  }
}

変更点は3点
1. コンストラクタの追加
2. renderメソッドのonClickプロパティ
3. {this.props.value} => {this.state.value} の書き換え
です

1.のステップを経て、コンストラクタ内でthis.stateに初期値を定義します。
(参考 コンストラクタとは:https://wa3.i-3-i.info/word13646.html)

2.のステップで、Squareコンポーネントがクリックされた際に、this.stateの値が'X'に置き換わるように変更を加えます。

3.のステップで、マスの中にthis.stateの値が表示されるように設定しています。

スクリーンショット 2020-05-21 16.29.11.png
マスをクリックして値が変更されればOKです。

また、constructorメソッドの中でsuper(props)を指定しますが、
なぜこれを書くのかはこちらの記事を参考にしてみてください。(翻訳してくださりありがとうございます。)

State のリフトアップ

ゲームの基本的な部品が揃いました。完全に動作するゲームにするためには、盤面に “X” と “O” を交互に置けるようにすることと、どちらのプレーヤが勝利したか判定できるようにする必要があります。

現状、各々のsquareが「null」であるべきか「X」であるべきかを、自身で判断しています。しかしこれでは勝敗はわかりません。
勝敗を判断するためには、盤面全体の状態を認識する鳥の目、つまり、Boardコンポーネントが必要になります。

方法として、BoardからSquareに対して現状の値を問い合わせるという方法もある(らしい)のですが、コードが壊れやすくわかりづらいものになってしまうのです。
なのでここでは、Boadsがマスの状態を管理し、それをpropsで各Squareに渡すことで、ゲームの管理をすることとします。

複数の子要素からデータを集めたい、または 2 つの子コンポーネントに互いにやりとりさせたいと思った場合は、代わりに親コンポーネント内で共有の state を宣言する必要があります。親コンポーネントは props を使うことで子に情報を返すことができます。こうすることで、子コンポーネントが兄弟同士、あるいは親との間で常に同期されるようになります。 
(公式ドキュメントから引用)

まずはBoardコンポーネントに変更を加えましょう。

index.js

class Board extends React.Component {

  ⬇︎ステップ①コンストラクタを追加
  constructor(props){
      super(props);
      this.state = {
          squares: Array(9).fill(null),
      };
  }

  ⬇︎ステップ③handleClickメソッドを追加
  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  ⬇︎ステップ②propsの内容を変更
  renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

順番に見ていきましょう。まずはコンストラクタについてです。
this.stateの中身に注目してください、

index.js ステップ①
  constructor(props){
      super(props);
      this.state = {
          squares: Array(9).fill(null),⬅︎ココ
      };
  }

squaresという名の、9つのnull要素を持つ配列を生成しています。
Squaresにあったコンストラクタとやっていることは一緒ですね。

続いて、renderSquareメソッドの変更を確認していきます。

index.js ステップ②
renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

ここで指定しているのは、Squareコンポーネントに実際に渡すpropsの値でしたね。
valueには、引数をインデックス番号としてsquaresから取り出した値を、
onClickには、BoardコンポーネントのhandleClickメソッドを指定しています。

そのhandleClickメソッドがこちら。

index.js ステップ③
handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

まず初めに、先ほどコンストラクタで生成した配列のコピーを作成しています(理由は後述)。
次に、クリックされたマスに対応する要素の値を、'X'に変更。
そしてstate.squaresを、値の変更が加えられたコピー配列である(const)squaresに置き換えています。

さて、この時点でSquareコンポーネントは、
"クリックされたらそれに反応してBoard内のhandleClickメソッドを呼び出す&props.valueを表示する"
だけの部品になりました、彼はもう自分で値を保持するような存在ではありません。
ここまできたらSquareコンポーネントのコンストラクタは削除しても大丈夫です。
また、renderメソッドのみを有して、自身のstateを持たないコンポーネントは、「関数コンポーネント」と呼ばれるものに置き換えられるので、こちらも書き換えましょう。

以下、現段階でのSquare/Boardコンポーネントです。

index.js
function Square(props) {
  return (
    <button className="square" onClick={props.onClick}>
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  constructor(props){
      super(props);
      this.state = {
          squares: Array(9).fill(null),
      };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square 
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

手番の処理

ここでは、Squareをクリックした際に、“X”と “O” が交互に出てくるように改良を加えます。

デフォルトでは、先手を“X”し指定します。Board のコンストラクタで state の初期値を変えればこのデフォルト値は変更できます。

index.js
class Board extends React.Component {
  constructor(props){
      super(props);
      this.state = {
          squares: Array(9).fill(null),
          xIsNext: true,                         ⬅︎ xIsNextを追加し、初期値にtrueを設定。
      };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O'; ⬅︎ xIsNextの真偽によって表示する値を変更
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,               ⬅︎ stateの値を反転
    });
  }

まず、BoardのコンストラクタにxIsNextという名の真偽値を設定し、初期値をtrueにします。
その次に、handleClickメソッド内で、Squareに渡すprops.valueに、xIsNextがtrueなら'X'を、falseなら'O'を代入します。
そのあとで、xIsNextの真偽値を反転させます。
この変更により、“X” 側と “O” 側が交互に着手できるようになります。

また、Boardコンポーネントのrenderメソッド内にある “status” テキストも変更して、どちらのプレーヤの手番なのかを表示するようにしましょう。

index.js
render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

スクリーンショット 2020-05-22 12.11.23.png
表示される値が変更されていればOKです。

ゲーム勝者の判定

ゲームが決着して次の手番がなくなった時にどちらが勝利したかを表示しましょう。また、既存のコードではすでに値が入ったSquareを再度クリックすると、値が反転してしまうので、これらの課題を解消していきます。

まずは、ファイル末尾に以下のヘルパー関数をコピーして貼り付けてください。

index.js
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];
    }
  }
  return null;
}

このヘルパーは、三目並べの勝利パターンである「タテ3・ヨコ3・ナナメ2」の合計8パターンに対して、同じ値が置かれているかを確認する役目を果たしています。もし勝者がいればその値を返し、いなければnullを返します。

このcalculateWinner(squares)ヘルパーを用いて、Boardコンポーネントのrenderメソッドを書き換えましょう。

index.js
render() {
    const winner = calculateWinner(this.state.squares);
    let status;                                                   
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      // the rest has not changed

ここではまず、winnerに現状のゲームの結果を代入します。
winnerに値が入っていれば'Winner: 勝者'という文字列をstatusに代入し、winnerの値がnullであれば'Next player: 'X'or'O''をstatusに代入します。

続いて、先ほど述べた「すでに値が入ったSquareを再度クリックすると値が反転してしまう」問題を解消します。
hundleClickメソッドを以下のように書き換えましょう。

index.js
handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

calculateWinner(squares)かsquares[i]のいずれかに値が入っている場合にはreturnします。
こうすることはif文以下の処理が実行される前にメソッドを終了することができるので、値が書き換えられるのを防ぐことができます。

スクリーンショット 2020-05-22 12.40.04.png
スクリーンショット 2020-05-22 12.40.13.png

盤面上部のテキストが変化し、埋まっているSquare(もしくは勝敗決定後)にクリックイベントが発生しなければOKです。

タイムトラベル機能の追加

以前の着手まで「時間を巻き戻す」ことができるようにしましょう。

そのためには、「着手の履歴」を保存しなければなりません、
ここで思い出して欲しいのですが、handleClickメソッドでは、常に元の配列のコピーを作成していました。

handleClick(i) {
    const squares = this.state.squares.slice();

このため、squares の過去のバージョンをすべて保存しておいて、過去の手番をさかのぼることができるようになります。

過去の squares の配列を、history という名前の別の配列に保存しましょう。
この history 配列は初手から最後までの盤面の全ての状態を記憶しており、以下のような構造を持っています。

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

State のリフトアップ、再び

上に示したhistory配列は、Gameコンポーネントで管理することにします。
早速、Game コンポーネントのstateにhistoryを設置しましょう

こうすることで、子であるBoardコンポーネントからsquaresのstateを、取り除くことができます。
Squareコンポーネントにあった「stateをリフトアップ」して Boardコンポーネントに移動したときと全く同様にして、今度は BoardにあるstateをトップレベルのGameコンポーネントにリフトアップしましょう。これにより GameコンポーネントはBoardのデータを完全に制御することになり、history内の過去の手番のデータをBoardに表示させることができるようになります。

まず、Gameコンポーネントの初期stateをコンストラクタ内でセットします。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }

  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

次に、Boardコンポーネントが squaresとonClickプロパティを Gameコンポーネントからpropsで受け取るようにします。
renderSquareメソッド内の{this.state.squares[i]}と{this.handleClick(i)}を以下のように書き換え、Boardコンポーネントのコンストラクタは削除しましょう。

index.js
class Board extends React.Component {
  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

  renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]} ⬅︎ココ
        onClick={() => this.props.onClick(i)} ⬅︎ココ
      />
    );
  }

Gameコンポーネントの内容を更新して、ゲームのステータステキストの決定や表示の際に最新の履歴が使われるようにします。
またGameコンポーネント側から、Boardに送るpropsの内容を設定します。以下にGameコンポーネントに加える変更をまとめて記載します。順を追ってその変更の意味を解説します。

まずは、BoardコンポーネントのhandleClickメソッドを、Gameコンポーネントに移行します。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }


  ⬇︎BoardhandleClickを移行
  handleClick(i) {
   const history = this.state.history;
   const current = history[history.length - 1];
   const squares = current.squares.slice();


    if (calculateWinner(squares) || squares[i]){
      return;
    }

    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
     history: history.concat([{
       squares: squares,
     }]),
      xIsNext: !this.state.xIsNext,
    });
  }

①:定数historyにstateのhistoryを代入
②:定数currentにhistory配列のうち最新のもの(index番号が最大のもの)を代入
③:定数squaresに、currentに含まれている盤面の状態をコピーして代入

※lengthは要素の数を返すので、起点は1です。対して配列の要素に割り当てられたindex番号は0を起点とするので、その差分を埋めるためにhistory.length 「-1」としています。

④:history配列に、変更が加わった新たな配列を生成し、追加。

続いて、renderメソッドの変更をしていきましょう。

index.js

  ⬇︎renderの内容を変更
  render() {
   const history = this.state.history;
   const current = history[history.length - 1];
   const winner = calculateWinner(current.squares);

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

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

    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>
⑥         <ol>{moves}</ol>
        </div>
      </div>
    );
  }
}

①・②:先ほどと同様。
③:定数winnerに、最新の盤面の勝敗状況を代入。
④:定数movesにmap関数を用いて生成されたボタンのリストを代入。
⑤:Boardに渡すpropsを設定。
⑥:④で作ったリストを表示

④のプロセスは私がやや混乱したポイントなので、丁寧に解説します。
見たらわかるという方は飛ばしてくださって構いません。

まずmap関数は、「指定された配列を参照して、特定の処理を施した新たな配列を作り出すメソッド」である
ということを念頭に、このコードを読み解いていきましょう。

ここでmapに引数として渡されているのは関数です。
その関数も引数を取っているのですが、
第一引数に元配列の要素を一つずつ呼び出し、
第二引数にその要素のインデックス番号が呼び出され、
要素一つづつに処理を加えたのちに、新たな配列が生成されます。

つまり、
stepにはhistory[0]からhistory[8]までのオブジェクトが、
moveには0から8までのindex番号が渡されています。

参考:https://teratail.com/questions/192578

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

ここまで理解すれば、このコードを読むことに抵抗は感じないはずです。

ゲームの履歴内にある三目並べのそれぞれの着手に対応して、ボタンを有するリストアイテムを作りました。
ボタンにはonClickハンドラがありthis.jumpTo()というメソッドを呼び出していますが、このメソッドは後ほど実装します。

Gameコンポーネントがゲームのステータステキストを表示するようになったので、対応するコードはBoard内のrenderメソッドから削除しましょう。Boardのrender関数は以下のようになります。

index.js(Boadコンポーネント)
render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }

スクリーンショット 2020-05-22 17.16.02.png
ボタンリストができればOKです。
また、jumpToメソッドが未定義なので、これらのボタンをクリックするとエラーが発生します。後ほどjumpToメソッドを定義します。

タイムトラベルの実装

着手に応じたボタンリストを先ほど作成しましたが、実はまだ不十分です。
それぞれのリストアイテムをReactが別個のものとして捉えるためには、識別子が必要になるからです。
人間から見れば、ボタンテキストに含まれる数字を見てリストのうちのどのアイテムなのか判断することができますが、Reactにはできません。

例えば、道端にいるスズメを個体毎に見分けるのは至難の技ですが、
別々の色がついたタグが足に括られていたとしたら、おそらく見分けられるでしょう。
Reactにも、このタグを用意する必要があります。
タグはReactにおいてkeyと呼ばれます。

三目並べゲームの履歴内においては、すべての着手にはそれに関連付けられた一意なIDが存在します。
着手はゲームの最中に並び変わったり削除されたり挿入されたりすることはありませんから、
着手のインデックスをkeyとして付与しましょう。

index.js
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>
      );
    });

まず、Gameコンポーネントのコンストラクタで、stateにstepNumber: 0を加えます。
これは、いま何手目の状態を見ているのかを表すのに使います。

index.js
class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

次に、GameコンポーネントにjumpToメソッドを定義して、stepNumberが更新されるようにします。
jumpToメソッドの引数には、リストボタンを作成した際のmove(つまり、history内の要素のindex番号)が置かれています。

また、更新しようとしているstepNumberの値が偶数だった場合はxIsNextをtrue に設定します。

index.js
handleClick(i) {
    // this method has not changed
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: (step % 2) === 0,
    });
  }

  render() {
    // this method has not changed
  }

次に、Gameコンポーネントのrenderを書き換えて、常に最後の着手後の状態をレンダーするのではなく stepNumber によって現在選択されている着手をレンダーするようにします。

index.js
render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    // the rest has not changed

最後に、マス目をクリックしたときに実行されるGameのhandleClickメソッドに、いくつか変更を加えます。

時間を巻き戻した時点でマス目がクリックされた場合にhistoryの配列 と stepNumberを更新する記述をします。

index.js
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) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      history: history.concat([{
        squares: squares
      }]),
    stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

slice(a, b)は、指定した配列のindex番号が、a以上、b未満の要素を持つ配列としてコピーします。
4手目まで進んだ局面があったとします。
0・1・2・3・4*
2手目まで戻りましょう。
0・1・2*・3・4
jumpToメソッドでは、history内の要素を検索して、該当する盤面をレンダリングするだけです。
この状態(stepNumberが2の状態)で新たにマス目がクリックされると、handleClickが動作し、

const history = this.state.history.slice(0, this.state.stepNumber + 1);

の部分によって、0以上3未満の要素を持つ配列のコピーが作成されます。
0・1・2*・3・4
⬇︎ slice(0, this.state.stepNumber + 1)
0・1・2*

こうすることで、現在の手'以降'のデータを持たない新たな配列でゲームをリスタートすることができます。
以前と同じように処理が流れ、新たな一手を追加した盤面のデータがhistoryに加えられます。
setStateのstepNumberに配列の長さを渡し、値を更新しましょう。

タイトルなし.gif
このように、ゲームの再スタートができるようになればOKです。

完成!!

以上でチュートリアルは終了です。お付き合いくださりありがとうございました。

後書き

1人で学習を進めていたら、わかったフリしてただろうなぁとか、前後のニュアンスでなんとなく使っていた関数も改めて調べると知らない点が多くて、普段いかに”なんとなく”で作業しているのかが自覚できました。
何か間違っている点があったら教えてくださると幸いです。

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

ReactのuseRefを使う上での注意点

はじめに

DOMのバウンディングボックスなどを取得する場合、hooksuseRefを使いたいケースがあります。しかし、下位のコンポーネントにそれらの情報を渡す場合などは、注意が必要になります。

次のようにメッセージの右下にいいねボタンを表示したいとします。
スクリーンショット 2020-05-23 10.21.52.png

うまくいかないパターン

ThumbsUpコンポーネントに対してボタン位置の基準となるHTMLElementを渡し、ThumbsUpにてボタンの表示位置を求めています。

App.tsx
import React, { useRef } from "react";
import ThumbsUp from "./ThumbsUp";
import "./styles.scss";

export default function App() {
  const ref = useRef<HTMLSpanElement | null>();
  return (
    <div>
      <span ref={ref} className="baloon">
        こんにちは
      </span>
      {ref.current && (
        <ThumbsUp anchor={ref.current} />
      )}
    </div>
  );
}

ThumbsUp.tsx
import React, { useRef, useEffect } from "react";
import "./styles.scss";

type Props = {
  anchor: HTMLElement;
};
export default function Portal({ anchor }: Props) {
  const ref = useRef<HTMLDivElement | null>(null);
  useEffect(() => {
    if (ref.current) {
      const anchorRect = anchor.getBoundingClientRect();
      const position = {
        top: anchorRect.top + anchorRect.height - 20,
        left: anchorRect.left + anchorRect.width - 10
      };
      ref.current.style.top = `${position.top}px`;
      ref.current.style.left = `${position.left}px`;
    }
  }, [anchor]);
  return (
    <div ref={ref} className="thumbs-up">
      <button>
        <span role="img" aria-label="thumbs-up">
          ?
        </span>
      </button>
    </div>
  );
}

何が起こるか

残念ながら、意図通りにいいねボタンが表示されません。
スクリーンショット 2020-05-23 10.19.35.png
これは公式にも記載されていますが、App.tsxにおいて、ref.currentが置き換わっても再レンダーは発生しないためです。そのため、ThumbsUpはレンダリングされないままの状態となっています。

解決策

コールバック式のrefuseStateを使うことで解決することができます。下のようにspanrefに関数を与えることで、setAnchorが呼ばれたタイミングで再レンダーを発生させ、意図通りThumbsUpをレンダリングできるようになります。

App.tsx
import React, { useState } from "react";
import ThumbsUp from "./ThumbsUp";
import "./styles.scss";

export default function App() {
  const [anchor, setAnchor] = useState<HTMLSpanElement>();
  return (
    <div>
      <span
        ref={elm => { // コールバック関数を与える
          setAnchor(elm);
        }}
        className="baloon"
      >
        こんにちは
      </span>
      {anchor && <ThumbsUp anchor={anchor} /> }
    </div>
  );
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む