20200824のReactに関する記事は15件です。

問い合わせをSlackに飛ばす【プログラミング初心者】

Next.jsで作ったポートフォリオサイトで問い合わせをSlackに飛ばしたい

転職に向けてNext.jsでポートフォリオサイト制作に精をだし中です。
今回はお問い合わせフォームを作り、その内容をSlackで受け取る機能を作りました。

初めに

今回参考にさせていただいたのは、私も参加しているエンジニアの学習コミュニティ「とらゼミ」で取り扱われた教材になります。

日本一わかりやすいReact入門【実践編】#9...問い合わせ用モーダルとSlack通知を実装しよう

開発環境

Next.jsのチュートリアルに習ってプロジェクトを作っていきます。
Next.jsチュートリアル

npx create-next-app my-portfolio --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"

next: 9.3.5
react: 16.13.1

my-portfolioという名前のプロジェクトが作成されました。

問い合わせフォームの作成

まずmaterial-uiのパッケージをインストール

お使いの作業ディレクトリで

$ npm install --save @material-ui/core

インストール!!

実装

TextInput.js
import TextField from '@material-ui/core/TextField';

const TextInput = props => {
  const { label, multiline, rows, value, type, onChange } = props;

  return (
    <TextField
      fullWidth={true}
      label={label}
      margin={"dense"}
      multiline={multiline}
      rows={rows}
      value={value}
      type={type}
      onChange={onChange}
    />
  );
}

export default TextInput;
ContactForms.js
import { react, useState, useCallback } from 'react';
import TextInput from "./TextInput";
import Button from '@material-ui/core/Button';
import { SendNotificationToSlack } from './SendNotificationToSlack';

const ContactForms = () => {
  const [name, setName] = useState(""),
        [email, setEmail] = useState(""),
        [description, setDescription] = useState("");

  // TextInputコンポーネントに渡すonChangeEvent達
  const inputName = useCallback((event) => {
    setName(event.target.value);
  });
  const inputEmail = useCallback((event) => {
    setEmail(event.target.value);
  });
  const inputDescription = useCallback((event) => {
    setDescription(event.target.value);
  });

  return (
    <div>
      <TextInput
        label={"お名前(必須)"} multiline={false} rows={1}
        value={name} type={"text"} onChange={inputName}
      />
      <TextInput
        label={"メールアドレス"} multiline={false} rows={1}
        value={email} type={"email"} onChange={inputEmail}
      />
      <TextInput
        label={"お問い合わせ内容"} multiline={true} rows={5}
        value={description} type={"text"} onChange={inputDescription}
      />
      <div>
        <Button
          onClick={SendNotificationToSlack(name, email, description)}
          variant="contained"
          color="primary"
          autoFocus
        >
          送信する
        </Button>
      </div>
    </div>
  );
}

export default ContactForms;

これで見た目の実装はOKです。
contactjs.png

Slackに通知を送る

Slackのワークスペース・チャンネルを開設

参考:Slackの使い方

  • ワークスペース作成

右上の「ワークスペースを作成する」を押し、繊維策の画面でメールアドレスを入力します。

ワークスペース開設.png

入力したアドレス宛に6桁のコードが届くのでそちらを入力すればワークスペースを開設できます。
new-workspace.png
できた!

「New WorkSpace」横からWebアプリで設定をいじれます。
ワークスペース名、表示言語、URLを変えました。

  • チャンネルの開設

▼Channelsの下のAdd channelsから作成
create-channel.png

必要な情報を入力しCreateをクリックでチャンネル開設です!

Incoming Webhookインテグレーションを追加

以下ページにアクセスし、post to channelで通知を送りたいチャンネルを選択します。
https://myportfolio-1995.slack.com/apps/new/A0F7XDUAZ-incoming-webhooks

add-webhooks1.png

するとWebhook URLを取得できます。

ここで取得できるURLは明かさない方がいいでしょう!

処理の実装

SendNotificationToSlack.js
import {WEBHOOK_URL} from '../../webhook/webhookConfig';
import fetch from 'node-fetch';

export const SendNotificationToSlack = (name, email, description) => {

  const payload = {
    text: 
    "お問い合わせがありました。\n" +"お名前: " + name +
    "\nEmail: " + email +
    "\n問い合わせ内容\n" +
    description
  }

  const url = WEBHOOK_URL;
  fetch(url, {
    method: 'POST',
    body: JSON.stringify(payload)
  }).then(() => {
    alert("お問い合わせの送信が完了致しました。");
  });
}

前述したとおりWebhook URLはソースコードに直接を載せるのではなく、別ファイルからexportして.gitignoreでgitに上げないようにした上で、必要なファイルでimportして使うようにしました。

お問い合わせを送ってみる

バリデーションやエラー時の処理とか書かれていませんが、実際にSlackに通知を送ってみましょう!

miss.png

あれれ、、ページにアクセスしただけで通知が送られてきちゃいました~?

renderごとに関数が実行されてフォームに1文字入力するごとに問い合わせが送られてくる迷惑極まりないバグ

コンポーネントのonClickに渡す関数が「functionName()」と()が付いていた為、renderごとに実行されてしまいました。

なので、ContactFormsコンポーネント内に以下の関数を作成し、
ボタンクリック→clickSendButton関数実行→SendNotificationToSlack関数実行という順番に処理を行うようにします。

ContactForms.js
import { react, useState, useCallback } from 'react';
import TextInput from "./TextInput";
import Button from '@material-ui/core/Button';
import { SendNotificationToSlack } from './SendNotificationToSlack';

const ContactForms = () => {
  const [name, setName] = useState(""),
        [email, setEmail] = useState(""),
        [description, setDescription] = useState("");

  // TextInputコンポーネントに渡すonChangeEvent達
  const inputName = useCallback((event) => {
    setName(event.target.value);
  });
  const inputEmail = useCallback((event) => {
    setEmail(event.target.value);
  });
  const inputDescription = useCallback((event) => {
    setDescription(event.target.value);
  });

  // 追加
  const clickSendButton = () => {
    SendNotificationToSlack(name, email, description);

    // 入力フォームを初期化
    setName("");
    setEmail("");
    setDescription("");
  }

  return (
    <div className="contact-form">
      <TextInput
        label={"お名前(必須)"} multiline={false} rows={1}
        value={name} type={"text"} onChange={inputName}
      />
      <TextInput
        label={"メールアドレス"} multiline={false} rows={1}
        value={email} type={"email"} onChange={inputEmail}
      />
      <TextInput
        label={"お問い合わせ内容"} multiline={true} rows={5}
        value={description} type={"text"} onChange={inputDescription}
      />
      <div className="contact-button-container">
        {/* 追加 */}
        <Button
          onClick={clickSendButton}
          variant="contained"
          color="primary"
          autoFocus
        >
          送信する
        </Button>
      </div>
    </div>
  );
}

export default ContactForms;

これで正常に動くはずです。

before.png
送信すると・・・?
after.png
Slackに通知が来ました!大成功なり!!!

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

react-router-domのプロパティ"exact"とは?

本記事を書いた経緯

Webアプリを開発するにあたって,ルーティングのためにreact-router-domを利用しました.
その中でも,プロパティのうちの一つであるexactがどのような役割なのか曖昧なまま利用していたのでこちらにまとめます.(Homeのpathに指定するものだと思い込んでいた...)

exactとは?

React Router<Route />内にexact(bool)を記述すると指定したpathとlocation.pathnameが「完全一致」した場合のみコンポーネントを返します.

ドキュメントの例をお借りすると,

path location.pathname exact maches?
/one one/two true no
/one one/two false yes

のようになります.
上の例では,exact=trueのため,完全一致でなければならずmachしません.
一方,下の例では,exact=falseのため,部分一致するone/twoとmachします.

Homeexactを指定する理由

私は,Homeで表示されるページにexactを指定すると勘違いしていましたが,上記のことを踏まえるとあながち間違えではないことが分かります.
つまり,

<Route exact path="/" component={Home} />

のようにexactを指定してあげないと.すべてのlocation.pathについてHomeのpathと部分一致してしまうからです.

参考

react-router

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

OpenAPIってなんだろう

はじめに

エンジニアインターンをして約3ヶ月が経とうとしています。
先日の業務でOpenAPIに初めて触れたので、OpenAPIとは何なのか、使い方等を見返せるようメモしていきたいと思います。

そもそもOpenAPIとは

あるサイトにこう書かれていました。

OpenAPIは、いわゆる「Web API」の仕様を形式的に記述するためのフォーマット

もう少し噛み砕くと、「バックエンドがまだ終わってない(タスクが未完)時にフロントエンドだけで実装(挙動)を確認するためのもの」です。

あたかも、バックからデータが返ってきているかのように見せることができます。実際はopenapi.yamlにゴリゴリ書かれているだけですが...

OpenAPIのいじり方

前提条件
localhostの番号はdocker-compose.ymlに書いてあるのでそこを参照
Swagger UIですがdockerイメージをを使用しています

1 envファイルを開き、NEXT_PUBLIC_API_SERVER_URL=https:~をNEXT_PUBLIC_API_SERVER_URL=http://localhost:9003 に変更する。

2 http://localhost:3000 で検索するとOpenAPIのexampleで作られたもののみで構成されたものが表示される。

3 直接openapi.yamlに記述するのではなく、Swagger UIというソフトウェアを使用。(http://localhost:9002/ を開いて編集する。(9002はswagger-editorのports番号を使用))
ちなみに、localhost:9001では記述はできないがswagger UIを見ることだけ可能

4 Swagger editorで編集したものをコピーして、openapi.yamlにペーストし、versionを1つあげる。(編集するたびにversionアップが必要です)
version: "0.4.9" だったら version: "0.4.10"に変更

5 http://localhost:3000/ を開き検証>Network>1の順に確認する。
image.png
dataを開いて、中にデータが入っていれば完璧!

最後に

openAPIを触ったことで、ロジック部分も学びました。しかし、理解が追いつかなかったため前提知識としてDDD、クリーンアーキテクチャの理解が必要だと感じました。

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

Next.jsのプリレンダリング方式についてまとめてみた

最近Next.jsを使い始めましたが、Next.jsにおいて重要な概念であるプリレンダリング方式(SSR・SSG)についてあまり理解できなかったため、公式ドキュメントを参考にそれぞれの方式の違いや使い分けについてまとめました。

この記事では、用語の説明にフォーカスしており、実装方法は記載してませんのでご了承ください。

プリレンダリングとは?

プリレンダリングとは、簡単にいうと事前にHTMLを生成することです。

通常のReactアプリケーション(SPA)の場合、ユーザーがWebページにアクセスし、Webページを表示する時にブラウザ側でHTMLを生成します。(クライアントサーバーレンダリング)

スクリーンショット 2020-08-24 16.46.19.png

プリレンダリングでは、ユーザーがアクセスする前に事前にHTMLを生成し、その用意されたHTMLをユーザーに提供する方式となっています。そのため、ブラウザの負荷を下げて表示を高速化することができます。

スクリーンショット 2020-08-24 16.49.33.png

また、事前にHTMLが生成されているため、検索エンジンのクローラーに全てのコンテンツを見せることができます。
SPAのSEO的なデメリットをカバーできることもプリレンダリングの強みの一つです。

Next.jsでは、デフォルトで全てのページでプリレンダリングが有効化されています。

プリレンダリング方式(SSR・SSG)の違い

Next.jsでは、2種類のプリレンダリング方式(SSR・SSG)があり、それぞれページごとに自由に選択して実装することができます。
この2つの方式の違いは、主にHTMLを生成するタイミングになります。

SSR(Server Side Rendering)

SSRでは、ユーザーがアクセスした時にサーバー側でHTMLを生成します。

SPAとの違いについてを簡単に説明しますと、

SPAではブラウザ側でHTMLを生成していましたが、SSRではサーバー側でHTMLを生成し、レンダリング済みのHTMLをブラウザ側に提供します。
要するに、ブラウザの大半の仕事をサーバー側に任せ、ブラウザの仕事は最後の描画だけとなります。

SSRは、リクエストごとにHTMLを生成するため、常に最新の状態をユーザーに見せることができます。

スクリーンショット 2020-08-24 16.50.14.png

SSG(Static Site Generator)

SSGでは、アプリビルド時にHTMLを生成します。

リクエストごとにHTMLを生成せず、事前にビルドされたHTMLを再利用する形となるため、SSRよりもさらに高速な表示が可能です。

スクリーンショット 2020-08-24 16.50.36.png

プリレンダリング方式(SSR・SSG)の使い分け

公式ドキュメントでは、以下の通り、基本的にはSSGを使用することが推薦されています。

We recommend using Static Generation (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.

意訳:可能な限りSSG(データの有無にかかわらず)を使用することをお勧めします。なぜなら、あなたのページは一度構築され、CDNによって提供されるので、サーバーがリクエストごとにページをレンダリングするよりもはるかに速くなります。

ただし、SSGはビルド時にHTMLを生成するため、更新頻度の高いページには適していません。

SNSや動画配信サイトといったリアルタイムにWebサイトの表示を変えたいページに関しては、SSRを選択するのが最適かと思います。

参考資料

Next.js 公式ドキュメント
nextjs with typescript:06 プリレンダリングとデータフェッチ

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

ReactのDropzoneでアップロードした画像をDjangoのOpenCVで顔検出して返すメモ

背景

大昔にドラッグアンドドロップでブラウザに画像を放り込むと
PHPで顔を検出して画像を返すシステムを作っていたので
それをReactとDjangoでやりたい

image.png

方法論

なんかReactではDropzoneってのがあるらしいのでそれを使う

image.png

Django-start

python -m venv serverで環境作った後
pipでDjangoとOpenCVを入れる
こんときにpipが古くてopencvが入んなかったのでpipをアップグレード

pip install -U pip
pip install django opencv-python

django-admin startproject server
cd server
python manage.py startapp image
python manage.py migrate

面倒だったからCRSF関連は捨てて
ビルドしたファイルをDjangoにそのまま突っ込む方式にした
後はと同じ

server/setting.py
ALLOWED_HOSTS = ["127.0.0.1"]

なお私は127.0.0.1でやってない

server/setting.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('image/', include('image.urls')),
]
image/urls.py
from django.urls import path

from . import views

app_name = 'urls'
urlpatterns = [
    path('', views.Index, name='index'),
    path('res/', views.Res, name='res'),
]

とりあえずimageアプリフォルダ内にstaticフォルダを作って
OpenCVの顔検出ファイルhaarcascade_frontalface_default.xml君を入れた(脳死)

またimageアプリフォルダ内にtemplates/imageも作成(次節で利用)

React-request

まずはdropzoneを入れる

npm install react-dropzone

先生方の投稿を拝見する限り--saveをつけた方がよいのだろう
よくわからんのでつけてない

そしてcodesandboxでひたすら実装

index.js
import React from "react";
import ReactDOM from "react-dom";

import Dropzone from "react-dropzone";

const SERVER = "http://127.0.0.1:8000/image/"; // サーバのポスト

class App extends React.Component {
  /**
   * 前回と同じ
   * @param {json} props
   */
  constructor(props) {
    console.log("初期化", props);
    super(props);
    this.state = {
      message: props.message
    };
  }

  /**
   * クッキーからCSRFトークンを取得
   */
  getCSRFtoken() {
    for (let c of document.cookie.split(";")) {
      //一つ一つ取り出して
      let cArray = c.split("="); //さらに=で分割して配列に
      if (cArray[0] === "csrftoken") return cArray[1]; // 取り出したいkeyと合致したら
    }
  }

  /**
   * ドロップされたときの処理
   * @param {dic} files = {path:}
   */
  handleOnDrop(files) {
    console.log("ドロップ", files);
    const cvs = document.createElement("canvas");
    const ctx = cvs.getContext("2d");
    const img = new Image();

    img.src = URL.createObjectURL(files[0]);
    img.onload = () => {
      cvs.width = img.width;
      cvs.height = img.height;
      ctx.drawImage(img, 0, 0, img.width, img.height);
      this.setState({ image: cvs.toDataURL(), message: React });
      this.render();
      this.submitData(); // 送信処理
    };
  }

  /**
   * サーバにデータを送信
   */
  submitData() {
    console.log("送信", this.state);
    fetch(SERVER + "res/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRFToken": this.getCSRFtoken()
      },
      body: JSON.stringify({
        image: this.state.image // ← ドロップされた画像を送信
      })
    })
      .then((res) => res.json())
      .then((res) => {
        console.log("受信", res);
        this.setState({ image: res.image, message: res.message }); // ← ドロップされた画像をセット
        this.render();
      });
  }

  /**
   * 画像をドロップする領域のレンダリング
   */
  renderDropzone() {
    return (
      <Dropzone
        ref={(node) => (this.dropzone = node)}
        onDrop={(acceptedFiles) => this.handleOnDrop(acceptedFiles)}
        accept="image/jpeg,image/png,image/jpg"
      >
        {({ getRootProps, getInputProps }) => (
          <section>
            <div {...getRootProps()}>
              <input {...getInputProps()} />
              <p>ここにファイルをドロップするかクリックでファイルを追加</p>
              <p>*jpgかpng</p>
            </div>
          </section>
        )}
      </Dropzone>
    );
  }

  /**
   * ボタンと送受信データのレンダリング
   */
  render() {
    return (
      <div>
        <div className="dropzone-element">{this.renderDropzone()}</div>
        <div className="image-element">
          {this.state.image ? <img src={this.state.image} alt="画像" /> : ""}
        </div>
      </div>
    );
  }
}

ReactDOM.render(
  <React.StrictMode>
    <App message={"Image"} />
  </React.StrictMode>,
  document.getElementById("root")
);

普通にJavaScript使ったらたいして時間かかんなかったから行けるだろうと取り組んだらクッソ時間かかった

まずは画像をドロップ領域する領域

index.js
    renderDropzone() {
        return (
            <Dropzone
                ref={(node) => (this.dropzone = node)}
                onDrop={(acceptedFiles) => this.handleOnDrop(acceptedFiles)}
                accept="image/jpeg,image/png,image/jpg"
            >
                {({ getRootProps, getInputProps }) => (
                    <section>
                        <div {...getRootProps()}>
                            <input {...getInputProps()} />
                            <p>ここにファイルをドロップするかクリックでファイルを追加</p>
                            <p>*jpgかpng</p>
                        </div>
                    </section>
                )}
            </Dropzone>
        );
    }

これが他の記事を見ると

{({ getRootProps, getInputProps }) => ( ... )}

の部分がなくてchildrenうんたらとかいう謎のエラーが出て死んだ
これは海外版知恵袋みたいなのであった回答を調べたら公式に載ってた[1]
仕様が変わったんですかね・・・?


ドロップされたときに画像をレンダリングする処理

index.js
    handleOnDrop(files) {
        const cvs = document.createElement("canvas");
        const ctx = cvs.getContext("2d");
        const img = new Image();

        img.src = URL.createObjectURL(files[0]);
        img.onload = () => {
            cvs.width = img.width;
            cvs.height = img.height;
            ctx.drawImage(img, 0, 0, img.width, img.height);
            this.setState({ image: cvs.toDataURL() });
            this.render();
            this.submitData(); // 送信処理
        };
    }

最初filesの中に画像のファイル名しか表示されなくてそこから画像を吸い出す方法がわからなかった(小並)
これもfetchでできねぇとかfile.previewってのがねぇとか苦行を強いられたが
公式が出してた「previewが使えなくなった」の声明ページにあったURL.createObjectURLを使った[2]
そんなんあったな
toDataURL関連は昔JavaScriptで作った奴を流用

画像をドロップするとfetchに失敗したメッセージが出てくる
エラーを無視すると図のような感じになるはず
image.png

以前と同様にnpm run-script buildして
buildファイルのindex.htmlをDjangoのimage/templates/image/に入れて
index.htmlの"/"{% static 'image/' %}/に置換し
先頭に{% csrf_token %}{% load static %}を挿入
さらにbuild内のそれ以外のファイルはimage/static/image/に移動

Django-view

こっからのviewもマヂ死んだ

view.py
from django.shortcuts import render
from django.http.response import JsonResponse
from django.http import HttpResponse

import json, base64

import numpy as np
import cv2

def Index(request):
    """
    Reactで作ったページを表示
    """
    return render(request, 'image/index.html')

def Res(request):
    """
    受信したデータから顔を検出して画像を応答

    Parameters
    ----------
    request.body : json dic
        {image: base64}となるJSON

    Returns
    -------
    response : JsonResponse
        {"image": base64}
    """
    data = request.body.decode('utf-8')

    jsondata = json.loads(data)

    # Base64をOpenCV形式に変換
    image_base64 = jsondata["image"]
    encoded_data = image_base64.split(',')[1]
    nparr = np.fromstring(base64.b64decode(encoded_data), np.uint8)
    image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)

    # 画像を正規化して顔を検出
    face_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    face_cascade = cv2.CascadeClassifier("image/static/haarcascade_frontalface_default.xml")
    faces = face_cascade.detectMultiScale(face_gray)

    for x, y, w, h in faces: # 顔領域に矩形を書き込み
        cv2.rectangle(image, (x, y), (x + w, y + h), (255, 0, 0), 2)

     # base64に変換
    result, dst_data = cv2.imencode('.png', image)
    dst_base64 = base64.b64encode(dst_data).decode()

    response = JsonResponse({
        "image": "data:image/png;base64,"+dst_base64,
        "message": "Django"
    })
    return response

前半のBase64をOpenCV形式に変換するところは昔やったことがあったからコピペしたが
作ったの半年くらい前だから何やってんのかまるでわからん
顔検出は園児でもできるので略

クッソ時間かかったのがndarrayをbase64に変換するとこ

view.py
def Res(request):
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     # 顔領域に矩形を書き込み
    result, dst_data = cv2.imencode('.png', image)
    dst_base64 = base64.b64encode(dst_data).decode()

    response = JsonResponse({
        "image": "data:image/png;base64,"+dst_base64,
        "message": "Django"
    })
    return response

未だに何故できたのか理解できてない
外国人兄貴曰く画像をクリーンな形でjsonにして返すのは無理だとかいう話を聞きながらPILとか使って苦行を重ねてたが
「cv2 Base64」とかでggり[3]にたどり着いたのだと思われる
"data:image/png;base64," + dst_base64のゴリラ感がヤバい

React-response

上記のレスポンスをユーザ側で受信し表示する画像を更新してレンダリング

index.js
  submitData() {
    console.log("送信", this.state);
    fetch(SERVER + "res/", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRFToken": this.getCSRFtoken() // CSRFトークン
      },
      body: JSON.stringify({
        image: this.state.image // 状態
      })
    })
    .then((res) => res.json())
    .then((res) => {
      console.log("受信", res);
      this.setState({ image: res.image });
      this.render();
    });
  }

結果

終わったときマジでこんな顔してた
image.png

結論

参考文献整理したら3件になったの草
50件くらい漁りましたよ(瀕死)

参考文献

[1]Carson Full,“react-dropzone,” 2020-08-24
[2]Carson Full,“Previews,” 2020-08-24
[3]@happou,“[Python3] 画像をBase64にエンコード、Base64をNumPy配列へ読み込みOpenCVで処理、画像データをBase64に変換,” 2020年02月03日

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

【鬼滅の刃】〜今日の呼吸ガチャ〜 を作りました

鬼滅の刃、面白いですね。

最近漫画とアニメを見終わったのですが、

ハマり過ぎて、

【鬼滅の刃 〜今日の呼吸ガチャ〜】

を作ってしまいました笑

今日の呼吸ガチャを回してみる↓
kimetsu-gacha.firebaseapp.com

ガチャを回すと、
今日のあなたの呼吸がわかります。

スクリーンショット 2020-08-23 17.17.06.png

スクリーンショット 2020-08-24 15.14.51.png

スクリーンショット 2020-08-24 15.15.11.png

スクリーンショット 2020-08-24 15.25.24.png

スクリーンショット 2020-08-24 15.26.25.png

使った技術

・React.js
・TypeScript
・Firebase

かかった時間

1日

なぜ作ったのか?

参考にしたWEBサービス

プルジャダイニング(本格ネパール料理店)menuガチャ

サイゼリヤ1000円ガチャ

過去に作ったアプリ

Qiitaの新着記事を、ニュース感覚で聞き流せるwebアプリ
聞い太?

作って感じたことを一言で

「好きこそものの上手なれ」

作るのは楽しい!

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

【鬼滅の刃】〜今日の呼吸ガチャ〜 を作りました

鬼滅の刃、面白いですね。

最近漫画とアニメを見終わったのですが、

ハマり過ぎて、

【鬼滅の刃 〜今日の呼吸ガチャ〜】

を作ってしまいました笑

今日の呼吸ガチャを回してみる↓
kimetsu-gacha.firebaseapp.com

ガチャを回すと、
今日のあなたの呼吸がわかります。

スクリーンショット 2020-08-23 17.17.06.png

スクリーンショット 2020-08-24 15.14.51.png

スクリーンショット 2020-08-24 15.15.11.png

スクリーンショット 2020-08-24 15.25.24.png

スクリーンショット 2020-08-24 15.26.25.png

使った技術

・React.js
・TypeScript
・Firebase

かかった時間

1日

なぜ作ったのか?

参考にしたWEBサービス

プルジャダイニング(本格ネパール料理店)menuガチャ

サイゼリヤ1000円ガチャ

作って感じたことを一言で

「好きこそものの上手なれ」

作るのは楽しい!

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

react-routerに乗りながら言語別URLに対応する

こんにちは!!! はなくら (@hanak1a_)です!!!???
今回は3DキャラクターSNS VRoid Hubで行った、「react-routerに乗りながら言語別URLに対応する」方法を紹介します!

皆さんreact-router使ってますか!? 使ってない!!? それはとてもイケイケですね! react-routerはしんどいのでやめましょう! 丁度いい代替のルーターライブラリはありませんが、Next.jsってやつに乗れれば大丈夫でしょう(知らんけど?‍♀️)

追記 2020/08/24 15:18 :: 代替ルーターライブラリないって言ったけど、記事を出した直後に Roconというライブラリを見つけました… ざっとDocsを見た感じよさげなので、気になったら覗いてみてください

ということで僕はまだreact-routerと袂を分かてていないのでreact-routerで言語別URL対応します?

言語別URL #とは

yourwebsite.me/ja, yourwebsite.me/enなど、URL内に言語コードを含むことで異なる言語向けのWebサイトを展開するアレのことを、この記事では言語別URLと呼んでいます。

VRoid Hubでは、/en/以下のURLからアクセスした場合に英語版ページを表示する仕組みに(さっき)しました。 これは海外向けSEOのためで、海外のGoogle Botから、「へぇ… 君くんって意外と英語対応してるんだ… (無言のインデックス)」をしてもらうために実装されています。

多言語対応のために言語別URLを提供することはGoogleからも推奨されています。

とりあえずルーティングをいい感じにやろうぜ

react-routerが/enあるなしを判断しつついい感じにルーティングを行えるかというと、VRoid Hubの環境下ではかなり微妙でした。

チュートリアル的なreact-routerの使い方のように、JSX内にルート定義をしているならreact-router-i18nを組み合わせた対応ができるのですが、VRoid HubではSSR時のAPIコール定義をまとめたかった都合上、react-router-configを使ったObject notation形式のルート定義を行っています。

const routeConfig: { [key in RouteName]: AppRouteConfig } = {
  RouteName.artworks: { // ← このsyntaxは不正だがハイライトが死ぬので…
    path: '/artwork/:id',
    exact: true,
    component: Artwork,
    preload: (match, dispatch) => [
      dispatch(fetchArtwork(match.params.id))
    ],
  },
  ...
}

/enを解釈できるようにするには、一段/:langをはさんであげればよいのですが、これをやると逆に現状のURL構造を/jaなどに変える必要が出てきます。

ここをいい感じにしようと思うと、react-routerで無理やりどうこうするより、ルーティングを自分でやってしまえという気持ちになりますね?

react-router-configは、ルートに対応するコンポーネントをレンダリングする処理をrenderRoutes(routeConfig)という関数が行っています。この関数を自分の好きなルーティング処理に置き換えてしまえば、いい感じにルーティングできそうです。

それでは、こちらのあらかじめ調理しておいたコードをご覧ください ?

// Router.ts
export const useRouteRender = () => {
  const location = useLocation();

  // `/[lang]`なURLを言語コードなしのURLに正規化する
  const normalizedPath = normalizeLocaleUrl(location.pathname);
  const match = useMemo(() => matchedRoute(routeConfig, normalizedPath), [normalizedPath]);

  const Component = match.component;
  const normalizedLocation = useMemo(
    () => ({
      ...location,
      pathname: normalizedPath,
    }),
    [location, normalizedPath],
  );

  return useMemo(
    () => ({
      renderRoutes: () => <Component match={match} location={normalizedLocation} />,
    }),
    [match, normalizedLocation],
  );
};

// App.tsx
export default () => {
  const { renderRoutes } = useRouteRender();

  return (
    <div>
      {renderRoutes()}
    </div>
  );
}

matchedRouteはreact-router-configが提供しているmatchedRoutesの「ジャストそれ!!!」なルート定義をとってきてくれる版です。配列返しません。

normalizeLocaleUrl()で言語コードあり・なしのURLを「言語コードなし」に正規化します。 アプリ側は言語コードなしのURLだと思い込みながら動くので、言語コード付きURLをルーター層に隠蔽することが出来ます。

Componentのpropsにmatch, locationを渡してあげれば、react-routerと同じインターフェースを保つ事ができます。

あとはSSR時にアプリで利用する言語を設定してあげればだいたい動きました。

server.tsx
// URLで指定された言語コードを取り出してくれるやつ
const lang = detectLangFromUrl(req.url);

reduxStore.dispatch(AppActions.setLanguage(lang));
i18n.changeLanguage(lang);

SSRからhydrateされたstateを元にStoreを復元→Storeに入っている言語設定を元にクライアント側でレンダリング、という流れでクライアント側は特に何もせず動きました。

クライアント側独自で言語設定を再解釈してる? それは大変だねぇ…… がんばってね、応援してるよ…(何???

URL生成も言語別URLに対応する

サイト内に点在するURL生成処理も言語別URLに対応する必要があります。
例えば、ユーザーが/en/artworksにいる時、そのページ内にあるサイト内リンクは全て/en/~形式になっていて欲しいですね。

VRoid HubのURL生成処理は全てmakePath / makeFullPathという単純な関数に任されており、これらの関数から現在のURL文脈を考慮することは、お行儀の都合上出来ません。(react-router下でwindow.locationを触りたくないよね)

そこで、これらの関数をReact Hooksでラップすることで、現在のURL文脈を考慮したURLを生成するように変更しました。

import { makePath, makeFullPath } from '昔々あるところにあったURL生成処理'

export const useUrlBuilder = () => {
  const { pathname } = useLocation();
  // pathnameから言語コード取り出してくれるマン
  const locale = detectLangFromUrl(pathname);
  const prefix = locale === Langs.En ? '/en' : '';

  return useMemo(
    () => ({
      makePath: (name: RouteName, data?: { [k: string]: string }, query?: { [k: string]: any }) =>
        `${prefix}${makePath(name, data, query)}`,
      makeFullPath: (name: RouteName, data?: { [k: string]: string }, query?: { [k: string]: any }) =>
        `${prefix}${makeFullPath(name, data, query)}`,
    }),
    [prefix],
  );
};

react-routerこわれちゃった…

さて、ここまでで何事もなくいい感じに動くようになったように見えますが、こんな強引なことをやればもちろんreact-routerがイカれます。
react-routerはブラウザに表示されてるURLを見て動こうとしますが、アプリ側は正規化済みのURLで動いているためです。

/en/~でアクセスされたらreact-routerは「対応パスなし」と判断しますので、useParams, useLocationなどのreact-routerビルトインのHooksはアプリ側が意図した状態を返しません。 ただしこれらのHooksはそんなに難しいことはしていないので、しれっと再実装できちゃいます。

なのでやっていきましょう。

Router.ts
import { useLocation as useReactRouterLocation } from 'react-router'
import qs from 'querystring'

export const useLocation = <T extends Record<string, string | string[]> = {}>(): Location<T> => {
  const location = useReactRouterLocation();
  const normalizedPath = normalizeLocaleUrl(location.pathname)
  const match = useMemo(() => matchedRoute(routeConfig, normalizedPath), [normalizedPath]);

  return useMemo(
    () => ({
      pathname: location.pathname,
      search: location.search,
      query: Object.assign(qs.parse(location.search.slice(1)), match?.params ?? {}),
    }),
    [location.pathname, location.search, match],
  );
};

export const useParams = <Params extends { [K in keyof Params]?: string } = {}>(): { [p in keyof Params]: string } => {
  const location = useLocation();
  const match = matchedRoute(routeConfig, location.pathname);

  return match?.params ?? {};
};

あとはアプリ内で使われているこれらのHooksを自前実装Hooksに置き換えればおわりです! こうして言語別URL対応ができました。 react-routerくん、""これからもよろしくな!""(悪い顔で睨みつける)

ここまでラップすると、いざ「react-routerやめよう!」となった時にも多少クッションにしやすいです。はやくNext.jsの恩恵を受けたいところですね▲

おわり

文中のコードには/en以外の言語が増えたときのこと何も考えてなさそうなコードが散らばっていてもにょっとしますね。 まあ必要になったときに考えればいいので雑に書いています。 importとかも雑に書いたり書かなかったりしてるので概念だけの説明でした。

参考になるかはわからんですが、参考になれば参考にしてください。
早くreact-routerやめて脳死dynamic importキメて配信されるjs軽くしたいアルヨ……?‍♀️

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

Reactでカスタムフックを作ってAPIを共通化する方法

最初はEndPointごとにAPIアクセス処理を作成しようとしていた

サーバ側のAPIでデータを取得しようとするときに、API毎にAPIアクセス処理を作成しようとしていました。下記のような感じです。

下記の例は、News情報をhttps://example.com/api/newsに取得し一覧に表示します。もしエラーが出たらerrorページに遷移させるというものです。

news.tsx
import { useEffect, useState } from "react";
import Router from "next/router";
import NewsApi from "newsApi";

const NewsPage = () => {
  const [list, setList] = useState<News[]>([]);
  useEffect(() => {
    const func = async () => {
      const res = await NewsApi.list();
      if (res.status !== 200) {
        Router.push("/error");
      } else {
        setList(res.data);
      }
    };
    func();
  }, []);
  return (
    <ul>
      {list &&
        list.map((n) => (
          <li key={n.newsID}>
            <p>{n.startTime}</p>
            <dl>
              <dt>{n.title}</dt>
              <dd>{n.description}</dd>
            </dl>
          </li>
        ))}
    </ul>
  );
};
newsApi.ts
import axios from "axios";
import News from "news";

class NewsApi {
  list = async () => {
    const res = await axios.get<News[]>("https://example.com/api/news", {
      headers: getAuthHeader(),
    }).catch((err) => {
        return err.response;
      });
    return res;
  };
}

export default new NewsApi();
news.ts
type News = {
  newsID: number;
  title: string;
  description: string;
  linkURL: string;
  startTime: Date;
  endTime: Date;
};

export default News;

カスタムフックで共通化する

私の作っているAPIの共通仕様として、ヘッダーに認証用のgetAuthHeader()を付与するというものがありました。
また、エラーページに遷移させるというのも各API共通のものとなります。
そうすると、毎回書くのは手間だったり、間違いが起こったりする可能性がありますので、API毎にAPIアクセス処理を用意するのではなく、共通化したものを用意した方がいいのではないかと。

カスタムフックと用いて実装したいと思います。

カスタムフックとは、名前が ”use” で始まり、ほかのフックを呼び出せる JavaScript の関数のことです。
https://ja.reactjs.org/docs/hooks-custom.html

ということで、useApi.tsを新たに作成しました。
axiosのインスタンス(httpClient)を使ってもらって、呼び出し元で定義するように修正しています。

useApi.ts
import axios, { AxiosResponse } from "axios";
import Router from "next/router";
import { useState, useEffect } from "react";

export let httpClient = axios.create({
  headers: getAuthHeader(),
});

const useApi = <T>(
  path: string,
  axiosFunc: () => Promise<AxiosResponse<T>>,
  initialState: T,
  handleError: ((res) => void) | null = null
): T => {
  const [data, setData] = useState<T>(initialState);
  useEffect(() => {
    const func = async () => {
      const res = await axiosFunc().catch((err) => {
        return err.response;
      });
      if (res.status !== 200) {
        handleError ? handleError(res) : Router.push("/error");
      } else {
        setData(res.data);
      }
    };
    func();
  }, []);
  return data;
};

export default useApi;

news.tsxはこう変わりました。

news.tsx
import News from "news";
import useApi, { httpClient } from "useApi";

const NewsPage = () => {
  const path = "https://example.com/api/news";
  const req = () => {
    return httpClient.get(path);
  };
  const list = useApi<News[]>(path, req, []);
  return (
    <ul>
      {list &&
        list.map((n) => (
          <li key={n.newsID}>
            <p>{n.startTime}</p>
            <dl>
              <dt>{n.title}</dt>
              <dd>{n.description}</dd>
            </dl>
          </li>
        ))}
    </ul>
  );
};

node.js-12.8.0 Typescript-3.9.7 next.js-9.4.4

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

React + SimpleBar: スクロールバーのスタイルをカスタマイズする

SimpleBarはスクロールバーをカスタマイズするライブラリです。スクロールバーを独自につくるのではなく、CSSのスタイルを割り当てるので、おかしな挙動は起こらず、ネイティブなスクロールのパフォーマンスが保たれます。あくまで、スクロールバーの見栄えを変えるだけです。

SimpleBar logo

デザインはCSSで定める

SimpleBarは純粋なCSSでスクロールバーのスタイルを定めます。CSSで与えられるスタイルでさえあれば、自由にカスタマイズできるということです。また、macOSとWindowsで同じ見た目になるのも大きな魅力といえます。

軽量なライブラリ

6KBのとても軽いライブラリです。JavaScriptはスクロールの動きそのものには触れません。ネイティブな動きとパフォーマンスが得られます。

モダンブラウザをサポート

ChromeとFirefox、Safariなどのモダンブラウザに加え、Internet Explorer 11をサポートします。

ライブラリの概要はドキュメントデモページでお確かめください。本稿と同じタイトルの「JavaScript + SimpleBar: スクロールバーのスタイルをカスタマイズする」でつくったつぎの作例は、標準のJavaScriptコードでSimpleBarのスタイルを割り当てました。「Left Column」にマウスポインタを重ねると、グラデーションのスクロールバーが現れます。今回のお題は、React用のSimplebarReactで同じサンプルをつくることです。

See the Pen JavaScript + SimpleBar: Customizing scrollbar style by Fumio Nonaka (@FumioNonaka) on CodePen.

本稿の作例は、Create React Appのひな形アプリケーションをもとにつくります。ひな形のつくり方については「Reactアプリケーションのひな形をつくる」をお読みください。

インストール

まず、SimpleBarをインストールします(「Installation」)。SimplebarReactもSimpleBarのCSSを使うからです。npmまたはyarnでインストールしてください。

npm install simplebar --save
yarn add simplebar

つぎに、SimplebarReactのインストールです。やはり、npmまたはyarnで行います(「Installation」)。

npm install simplebar-react --save
yarn add simplebar-react

基本となるページの組み立て

ページを構成する要素は大きく3つ、ヘッダと左カラム、そしてメインコンテンツです(図001)。また、Bootstrap 4.5を用いました。ただし、本稿ではCSSの説明は基本的に省き、SimpleBarの扱いに関わる定めだけ解説することにします。確かめたい方は、最後に掲げるCodeSandboxのサンプルまたはGithubのソースをご覧ください。

図001■ヘッダと左カラムにメインコンテンツで組み立てられたページ

2007001_002.png

以下のコードは、アプリケーション(src/App.js)に、それぞれヘッダ(src/components/Header.js)と左カラム(src/components/LeftColumn.js)およびメインコンテンツ(src/components/MainContents.js)を静的にレイアウトしたモジュールの中身です。src/App.jssimplebar/dist/simplebar.min.cssを読み込んでいます。

SimplebarReactのJavaScriptライブラリをimportするのは、スクロールバーをカスタマイズする左カラムのモジュール(src/components/LeftColumn.js)です。なお、リストの連番項目は、メソッドArray.from()Array.prototype.map()でつくりました。興味のある方は「ECMAScript 6のArrayに関わる構文を試す」をお読みください。

src/App.js
import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";
import "simplebar/dist/simplebar.min.css";
import "./App.css";
import Header from "./components/Header";
import MainContents from "./components/MainContents";
import LeftColumn from "./components/LeftColumn";

function App() {
  return (
    <div className="App">
      <Header />
      <div className="container-fluid d-flex px-0">
        <LeftColumn />
        <MainContents />
      </div>
    </div>
  );
}

export default App;
src/components/Header.js
import React from "react";

const Header = () => {
  return (
    <header id="header" className="text-white bg-primary w-100 p-2 d-flex">
      <h1>Header</h1>
    </header>
  );
};

export default Header;
src/components/LeftColumn.js
import React from "react";

const LeftColumn = () => {
  return (
    <div id="left-column" className="bg-light p-2">
      <h3>Left column</h3>
      <ul id="list" className="pl-4">
        {Array.from(new Array(20), (_, index) => (
          <li key={index}>item {String(index + 1).padStart(2, 0)}</li>
        ))}
      </ul>
    </div>
  );
};

export default LeftColumn;
src/components/MainContents.js
import React from "react";

const MainContents = () => {
  return (
    <main className="px-4 py-2">
      <h2>Main contents</h2>
      <p>
        <!-- [中略] -->
      </p>
    </main>
  );
};

export default MainContents;

ヘッダを上部に固定する

まずは、ヘッダをページ上部に固定するCSSの設定です(src/App.css)。位置はpositionプロパティにfixedを与えて固定します。具体的な置き場所は上部なのでtop: 0です。すると、<body>要素の領域に含まれなくなるので、そのままではページの上部がかぶって隠れてしまいます(図003)。

src/App.css
#header {
  position: fixed;
  top: 0;
}

図002■ページ上部をヘッダが覆ってしまう

2007001_003.png

<body>要素のpaddingまたはmarginは、ヘッダの高さ分下げなければならないのです。もっとも、高さはウィンドウ幅やレスポンシブの設定によって変わるかもしれません。動的に定めるべきでしょう。

要素の高さはelement.clientHeightで得られます。要素を参照するフックはuseRefです(src/App.js)。参照(header)は、プロパティでヘッダのコンポーネント(src/components/Header.js)に渡します。ただし、element.clientHeightは、読み取り専用プロパティであることにご注意ください。高さの設定には、要素(<div>)のstyle属性を用います。

src/App.js
// import React from "react";
import React, { useEffect, useRef, useState } from "react";


function App() {
  const [headerHeight, setHeaderHeight] = useState(0);
  const header = useRef(null);
  useEffect(() => {
    const _header = header.current;
    const setLayout = () => {
      setHeaderHeight(_header.clientHeight);
    }
    window.addEventListener("resize", setLayout);
    setLayout();
  }, []);
  return (
    // <div className="App">
    <div className="App" style={{ paddingTop: headerHeight }}>
      {/* <Header /> */}
      <Header headerRef={header} />

    </div>
  );
}
src/components/Header.js
// const Header = () => {
const Header = ({ headerRef }) => {
  return (
    <header
      id="header"
      ref={headerRef}

    >
      <h1>Header</h1>
    </header>
  );
};

SimpleBarを組み込む

SimpleBarを使う要素には、overflowプロパティにautoを与えてください(src/App.css)。そのうえで、SimpleBarを設定する要素は<SimpleBar>に置き替えます(src/components/LeftColumn.js)。

src/App.css
#left-column {
  overflow: auto;
}
src/components/LeftColumn.js
import SimpleBar from "simplebar-react";

const LeftColumn = () => {
  return (
    // <div id="left-column" className="bg-light p-2">
    <SimpleBar id="left-column" className="bg-light p-2">

      {/* </div> */}
    </SimpleBar>
  );
};

そして、スクロールバーを表示するには、要素に高さを定めなければなりません。

src/App.css
#left-column {
  overflow: auto;
  height: 400px;  /* 高さを定める */
}

とはいえ、高さを決め打ちは避けたいところです。すでに、ヘッダの高さはとれるのですから、ブラウザウィンドウのビューポートの高さ(window.innerHeight)から差し引けば、左カラムの高さ(leftColumnHeight)は求まります。

src/App.js
function App() {

  const [leftColumnHeight, setLeftColumnHeight] = useState(0);

  useEffect(() => {

    const setLayout = () => {

      setLeftColumnHeight(window.innerHeight - _header.clientHeight);
    };

  }, []);
  return (
    <div className="App" style={{ paddingTop: headerHeight }}>

      <div className="container-fluid d-flex px-0">
        {/* <LeftColumn /> */}
        <LeftColumn leftColumnHeight={leftColumnHeight} />

      </div>
    </div>
  );
}
src/components/LeftColumn.js
// const LeftColumn = () => {
const LeftColumn = ({ leftColumnHeight }) => {
  return (
    // <div id="left-column" className="bg-light p-2">
    <SimpleBar
      id="left-column"
      className="bg-light p-2"
      style={{
        height: leftColumnHeight,
      }}
    >

      {/* </div> */}
    </SimpleBar>
  );
};

これで、ウィンドウに合わせて左カラムの高さが定まり、スクロールバーは自動表示されるようになりました(図003)。

図003■SimpleBarのスクロールバーが自動表示される

2008002_001.png

ページ全体をスクロールしたときの不具合を直す

まだ少し、不具合が残っています。ページ全体を下にスクロールしたとき、左カラムがせり上がって、ヘッダにかぶってしまうことです(図004)。

図004■ページを下にスクロールすると左カラムがヘッダにかぶる

2008002_002.png

左カラム(src/components/LeftColumn.js)の垂直位置は固定しなければなりません。やり方は、前述「ヘッダを上部に固定する」と同じです。ただ、CSS(src/App.css)でなく、style属性で定めることにしました。

src/App.js
function App() {

  return (
    <div className="App" style={{ paddingTop: headerHeight }}>

      <div className="container-fluid d-flex px-0">
        <LeftColumn
          headerHeight={headerHeight}

        />

      </div>
    </div>
  );
}
src/components/LeftColumn.js
// const LeftColumn = ({ leftColumnHeight }) => {
const LeftColumn = ({ headerHeight, leftColumnHeight }) => {
  return (
    <SimpleBar

      style={{
        position: "fixed",
        top: headerHeight,

      }}
    >

    </SimpleBar>
  );
};

もちろん、前述「ヘッダを上部に固定する」と同じように、左カラムがメインコンテンツにかぶってしまいます(図005)。

図005■メインコンテンツが左カラムに隠れてしまう

2008002_003.png

「ヘッダを上部に固定する」と同じ考え方で、メインコンテンツの左端をカラムの幅だけ右に寄せればよいはずです。けれど、つぎのコードではメインコンテンツの位置がまったく動きません。

src/App.js
function App() {
  const [leftColumnWidth, setLeftColumnWidth] = useState(0);

  const leftColumn = useRef(null);
  useEffect(() => {

    const _leftColumn = leftColumn.current;
    const setLayout = () => {

      setLeftColumnWidth(_leftColumn.clientWidth);

    };

  }, [leftColumn]);
  return (
    <div className="App" style={{ paddingTop: headerHeight }}>

      <div className="container-fluid d-flex px-0">
        <LeftColumn
          leftColumnRef={leftColumn}

        />
        {/* <MainContents /> */}
        <MainContents leftColumnWidth={leftColumnWidth} />
      </div>
    </div>
  );
}
src/components/MainContents.js
// const MainContents = () => {
const MainContents = ({ leftColumnWidth }) => {
  return (
    // <main className="px-4 py-2">
    <main className="px-4 py-2" style={{ marginLeft: leftColumnWidth }}>

    </main>
  );
};
src/components/LeftColumn.js
// const LeftColumn = ({ headerHeight, leftColumnHeight }) => {
const LeftColumn = ({headerHeight, leftColumnHeight, leftColumnRef }) => {
  return (
    <SimpleBar

      ref={leftColumnRef}

    >

    </SimpleBar>
  );
};

SimpleBarコンポーネントをラップする

調べてみると、SimpleBarコンポーネントのclientWidthプロパティ値が0です。SimpleBarは、あくまでスクロールバーのスタイルを整えるためのラッパーだからでしょう。

そこで、Simplebarコンポーネントをつぎのように<div>要素で包み、スクロールバーに用いる以外の属性はすべてこの要素に移します。こうすることで、カラムの要素の幅(clientWidth)が正しく得られるのです。メインコンテンツの左端は、カラムの右端に揃います。

src/components/LeftColumn.js
const LeftColumn = ({ headerHeight, leftColumnHeight, leftColumnRef }) => {
  return (
    <div
      id="left-column"
      ref={leftColumnRef}
      className="bg-light p-2"
      style={{
        position: "fixed",
        top: headerHeight,
      }}
    >
      <SimpleBar
        /* id="left-column"
        ref={leftColumnRef}
        className="bg-light p-2" */
        style={{
          /* position: "fixed",
          top: headerHeight, */
          height: leftColumnHeight,
        }}
      >

      </SimpleBar>
    </div>
  );
};

CSSでスクロールバーのスタイルを変える

SimpleBarのスクロールバーのスタイルは、CSSにより定められています。つまり、見栄えがCSSで変えられるということです。ここでは、スクロールさせるスライダのカラーを、つぎのCSSでグラデーションにしてみましょう(図006)。

src/App.css
.simplebar-scrollbar::before {
  background: linear-gradient(darkblue, skyblue);
}

図006■メインコンテンツの位置が正しく定まってスライダはグラデーションになった

2008002_004.png
>> CodeSandboxへ

冒頭の標準JavaScriptのサンプルと同じページをSimplebarReactでつくり、CodeSandboxに掲げました。また、Githubでもソースをご覧いただけます。

 

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

【React】カウンターAppでuseState, useEffectを使ってみた

はじめに

React Hooks(useState, useEffect)の使い方をカウンターAppでデモしながらご紹介します。

useStateとuseEffectとは

useState

useStateは関数コンポーネントにstateを持たせることができ、コードは下記の通りです。

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

第一引数: 現在のstate
第二引数: stateを更新するための関数(呼び出し用の関数)

state, setState, initialStateは任意の名前を付けられます。

useStateは現在の state と、それを更新するための関数を、ペアにして返します。
なので分割代入をしています。

例えばstateに1を追加したい場合は、setState(state+1)で実行できます。

useEffect

useEffectは副作用を実行することができ、基本の形は下記のとおりです。

useEffect(() => {
    effect
    return () => {
      cleanup
    }
  }, [input])

機能としては第二引数([input])に変更があった場合に第一引数(関数部分)を実行することというものです。第二引数は配列[]で指定します。

第二引数が空配列[]の場合、初回レンダリングのみ関数が呼ばれます。
また第二引数が省略された場合、レンダリングした際に毎回関数が呼ばれます。

Appを作成

ターミナル
$ npx create-react-app count-app --typescript               

まず、useStateを用いてCounterAppを作成します。

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

function CountApp() {

  const[ count, setCount ] = useState(0);

  return(
    <>
      <h1>Counter App</h1>
      <h2>{count}</h2>
      <button onClick={()=> setCount(count+1)}>+ number1</button>
    </>
  );
}

export default CountApp;

useState.gif

次にuseEffectでconsoleにログを出してみます。

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

function CountApp() {
  const[ count, setCount ] = useState(0);

  useEffect(() => {
    console.log("Add number1")
  }, [count])

  return(
    <>
      <h1>Counter App</h1>
      <h2>Count: {count}</h2>
      <button onClick={()=> setCount(count+1)}>+ number1</button>
    </>
  );
}

export default CountApp;

useEffectの部分を正式なフォーマットに倣うと下記のコードになります。
今回はログを出すだけなので、宣言せずにそのまま使用しました。

useEffect(() => {
    const log = () => {console.log("add")};
    return () => {
      log();
    };
  }, [count])

useEffect.gif

useStateとuseEffectは複数使用可能

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

function CountApp() {
  const[ count, setCount ] = useState(0);
  const[ count2, setCount2 ] = useState(0);


  useEffect(() => {
    console.log("Add count")
  }, [count])

  useEffect(() => {
    console.log("Add count2")
  }, [count2])

  return(
    <>
      <h1>Counter</h1>
      <h2>count: {count}</h2>
      <button onClick={()=> setCount(count+1)}>+ number1</button>
      <h2>count2: {count2}</h2>
      <button onClick={()=> setCount2(count2+1)}>+ number2</button>
    </>
  );
}

export default CountApp;

counter.gif

番外編

初期値の切り出し

Typescriptっぽく初期値を切り出してみました。

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

interface Props {
  initialCount: number;
}

function CountApp({initialCount}: Props) {
  const[ count, setCount ] = useState(initialCount);

  useEffect(() => {
    console.log("Add")
  }, [count])

  return(
    <>
      <h1>Hooks</h1>
      <h2>Count: {count}</h2>
      <button onClick={()=> setCount(count+1)}>+</button>
    </>
  );
}

export default CountApp;

関数の切り出し

setCountをreturnの外で定義してみました。

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

function CountApp() {
  const[ count, setCount ] = useState(0);

  const addCount1 = () => {
    setCount(count + 1);
  }

  useEffect(() => {
    console.log("Add number1")
  }, [count])

  return(
    <>
      <h1>Couner App</h1>
      <h2>number1: {count}</h2>
      <button onClick={addCount1}>+</button>
    </>
  );
}

export default CountApp;

終わりに

かなり初歩的な使い方ですが、useStateとuseEffectの使い方をデモしてみました。

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

MicroCMS + Gatsby.js(React) + Netlifyでポートフォリオサイトを作ってみた

概要

ポートフォリオサイト作って、いろいろと学んでみました。
備忘録もかねて残していきます。誰か参考になればと思います。

最終アウトプット

やりたかったこと

  • Jamstackでサイト作りたい
  • Adobe XD使いたい
  • Reactやりたい(Vueとの違いを体験したい)
  • Typescriptやりたい(通常のJSとの違いを体験したい)

対象者

  • なくても大丈夫ですが、都度調べるのでもう少し時間はかかると思います
  • Jamstackなサイトを作りたいけどはじめて
  • フロントはある程度できる
    • マークアップはある程度できる(CSS FWは使わずに実装できる)
    • React/Vue/Angularなどのモダンフロントを経験したことがある
  • Gitできる

自分のレベル感

  • Web系のSI会社勤務(2020.08現在)
  • デザイン・ワイヤー・モックも作ったことない
    • Adobe XDで作られたモックからからマークアップはやったことある
  • フロントはちょっとだけできる
    • HTML/CSS, JS, Vue
    • Reactはやったことない
  • バックエンドはまぁできる
    • Rails, Node.jsなど
    • GraphQLはAppSync経由で使ったことあるので今回の学習コストはなし
  • 一人で一からサービスをリリースみたいなのは経験ない
    • プロジェクトではあるが、全部に関わっているわけではないので

作業の流れ

ワイヤーの作成

  • はじめからXDみたいなデザインツール、あるいはマークアップをやろうとすると逆に時間がかかると思います.デザインは飛ばすとしてもサクッとワイヤーは作っておきましょう.
  • いろいろなポートフォリオサイトを参考に作りました
  • 手書きでざっくり書く
  • レスポンシブに作る場合はスマホのワイヤーも作る
  • コンポーネント設計はこの段階で軽くやっておく
  • 以下は実際に作成したものの一部です(それぞれOverviewページのPCとスマホ)

モックの作成

  • Adobe XDで作成する
    • 他に実際の業務でよく使われているのはFigmaのイメージです.現職はモックがあるときはXDがほぼ100%(イラレはつらみ)だったのでXDを採用しました
  • 事前にAdobeにあるチュートリアルをSTEP2までやりました
    • こちらはAdobeXDのバージョンが古すぎてあまり参考になりませんでした。
  • ちづみさんのサイトが参考になりました

クロームの拡張Window Resizer で予定のカンプの幅にブラウザを変更

クロームの拡張Full Page Screen Captureでスクショ

XDの拡張Mimicにurlをコピペ

スクショを複製し1枚はトレース用に薄くひく

サイトの検証も見ながら数値等を確かめつつ模写

  • ある程度デザインの決め事をやっておく
- marginは4の倍数
- font-sizeは12, 14, 16, 20, 34を基本にする(その他は以下参考)
  - https://material.io/design/typography/the-type-system.html#type-scale
  - 入力欄は14px以下にしない(スマホでズーム問題が発生する)
- ナビの高さは50pxから100px
- コンテンツ幅は900pxから1180px
  • 以下は実際に作成したものです
    • デザイン慣れている人は多分数時間でできるんだろうなーと思います
    • レスポンシブに作るのがかなり大変でした.でも一度作っておくとマークアップするときにも役に立つと思います image.png

フロントでどんな技術を使うか考察

  • フロントでやりたいことは以下
    • GraphQL
    • React
    • Typescript
    • Jamstack
    • 無料枠内に収まる
  • 結果採用したのは以下のとおり
    • ヘッドレスCMS: microCMS(似たサービスはstrapiとかContentfulなど)
    • FW: Gasby.js(似たFWはNext.jsなど)
    • ホスティング: Netlify(似たサービスはGithub Pagesなど)
  • ReactでJamstackで調べると、Next.js + Netlifyがでてきたのですが、 GraphQLが対応していないらしく、Gatsby.jsを使うことにします。GatsbyもGithubのスターの数あんまり変わらないくらいで結構有名みたいです。
    • Next.jsとGatsby.jsの違いはこちらが参考になりました。

フロントの実装

microCMSの設定

  • microCMSは(実際は違うけど)データベース + APIみたいなイメージ
  • アカウント登録
  • サービス情報を入力.今回はプロフィールサイトを作りたいので以下で登録しました
    • サービス名: [名前]のプロフィール
    • サービスID: profile-koyama.microcms.io
  • APIを追加
    • 以下の部分を取得するAPIを作って見ます image.png
    • データの定義とAPIのエンドポイントを同時に登録します.今回の例では名前や年齢などのことです
    • API名とエンドポイントを入力して次へ image.png
    • APIの型は単一データなので、オブジェクト形式を選択 image.png
    • APIスキーマ(テーブル定義的なもの)を作成します.ユーザ情報に必要なものを登録していきます
    • 型は普通のテキストや数値からDBだけだと難しい画像などまでいけます
    • 関係はないですが、ageはアンチパターンですね。例えば生年月日のようにコトを設定する方がよいです image.png
  • データを登録
    • サイドバーから作成したAPI(ユーザ情報)を選択
    • コンテンツ管理というタブが選択されているので、そこにデータを登録 image.png
    • APIプレビューを選択 > 取得を選択でレスポンスを確認(以下のようにAPI叩いたらJSONで返ってきます) image.png
  • 以下のHOBBYのような複数の項目を登録したい場合はAPIの型をリスト形式で作ればOKです image.png
    • 作ったイメージが以下です image.png
    • JSON配列で取得することができます image.png
    • api名/:idで単一で取得することもできます(REST的な考え方) image.png
    • Favoriteは趣味に固定値として持ってもよいですが、複数コンテンツ参照を使うことで趣味の下にぶら下げることができます(完結にするために今回最終的には固定値で持つようにしました)

Gatsbyで構築

  • GatsbyとはReactベースのSSG(Static Site Generator: 静的サイトジェネレータ)のこと.一言でいうと爆速なサイトが簡単に作れるようになる

プロジェクトの作成

  • Gatsbyをインストールして、プロジェクトの作成
    • 途中でパッケージマネージャを聞かれますが、Yarnを選択しました.
$ npm install -g gatsby-cli
$ gatsby new my-profile https://github.com/gatsbyjs/gatsby-starter-default
(省略)
✔ Which package manager would you like to use ?
yarn
(省略)
Your new Gatsby site has been successfully bootstrapped. Start developing it by running:

  cd my-profile
  gatsby develop
  • とりあえずプロジェクトの作成が成功したか見るために上記のコマンドを実行し、画面を見ておきます
$ cd my-profile
$ gatsby develop # yarn developでもOK
(省略)
success Building development bundle - 11.356s
# http://localhost:8000/ に接続します

image.png

  • 今後のためにGitHubにpushしておきましょう
    • GitHubのリポジトリを追加してpush
$ git remote add origin https://github.com/naoto-koyama/my-profile.git
$ git push -u origin master

フォルダ構成

.
├── LICENSE
├── README.md
├── gatsby-browser.js # ブラウザサイドの設定.共通CSSの設定など
├── gatsby-config.js # インストールしたプラグインの設定、サイトのメタデータやタイトル等の設定など
├── gatsby-node.js # 動的なページを作成する際に設定.$ gatsby buildを実行したときに走る処理
├── gatsby-ssr.js # SSR関連の処理を設定
├── node_modules
├── package.json
├── public
|   ├── icons
|   ├── page-data
|   └── static
├── src
|   ├── components # コンポーネントの設定
|   ├── images # 画像ファイルを置いておく
|   └── pages # 各ページを設定.URLとファイル名が一致する
└── yarn.lock

microCMSとの連携

  • プラグインの追加
$ yarn add gatsby-source-microcms
  • gatsby-config.jsの設定の修正
gatsby-config.js
plugins: [
   `gatsby-plugin-react-helmet`,
   {
     resolve: `gatsby-source-filesystem`,
     options: {
       name: `images`,
       path: `${__dirname}/src/images`,
     },
   },
     --- (中略) ---
   {
     resolve: "gatsby-source-microcms",
     options: {
       apiKey: "********-****-****-****-********e8b9", // microCMSのX-API-KEY
       serviceId: "profile-koyama", // はじめに設定したserviceId
       endpoint: "skills", // APIのエンドポイント
     },
   },
   {
      resolve: "gatsby-source-microcms",
      options: {
        apiKey: "********-****-****-****-********e8b9",
        serviceId: "profile-koyama",
        endpoint: "userinfo",
        format: 'object', // オブジェクト形式の場合必要
      },
    },
     --- (中略) ---
 ],
}
$ gatsby develop
# http://localhost:8000/___graphqlに接続
  • GraphQL経由でmicroCMSのデータを取得できることが分かります
    image.png

  • API KeyがGitリポジトリ上で確認できるのはまずいので、環境変数として持っておきます.ローカルとNetlify両方いずれも対応させるにはプレフィックスにGATSBY_をつける必要があるようです

".env"
GATSBY_MICRO_CMS_API_KEY=********-****-****-****-********e8b9
gatsby-config.js
require("dotenv").config() // 追加
plugins: [
   `gatsby-plugin-react-helmet`,
   {
     resolve: `gatsby-source-filesystem`,
     options: {
       name: `images`,
       path: `${__dirname}/src/images`,
     },
   },
     --- (中略) ---
   {
     resolve: "gatsby-source-microcms",
     options: {
       apiKey: process.env.GATSBY_MICRO_CMS_API_KEY, // 環境変数に書き換え
       serviceId: "profile-koyama",
       endpoint: "skills",
       readAll: true, // デフォルトでは最大10件
     },
   },
     --- (中略) ---
 ],
}

Scoped CSSの導入

  • Vue.jsではデフォルトで入っているSCSSとScoped CSSを導入します
  • 以下でSCSSが使えるようになります
$ yarn add node-sass gatsby-plugin-sass
gatsby-config.js
module.exports = {
     --- (中略) ---
plugins: [
    `gatsby-plugin-sass`, // 追加
     --- (中略) ---
  • 試しにindex.jsでのみに適用するSCSSを作成します.ページ名に合わせてindex.module.scssとします
index.module.scss
.hoge {
  color: green;
}
  • index.jsで読み込みます
index.js
import React from "react"
import { Link } from "gatsby"

import Layout from "../components/layout"
import Image from "../components/image"
import SEO from "../components/seo"

import styles from "./index.module.scss" // 追加


const IndexPage = () => (
  <Layout>
    <SEO title="Home" />
    <h1>Hi people</h1>
    {/* classNameを追加 */}
    <p className={styles.txt}>Welcome to your new Gatsby site.</p>
    <p>Now go build something great.</p>
    <div style={{ maxWidth: `300px`, marginBottom: `1.45rem` }}>
      <Image />
    </div>
    <Link to="/page-2/">Go to page 2</Link> <br />
    <Link to="/using-typescript/">Go to "Using TypeScript"</Link>
  </Layout>
)

export default IndexPage

  • 画面上でもスタイルが当たって緑色になっていることを確認できます
    image.png

  • Reset.cssのようなものをimportする際にMixinするファイルも存在する場合にはGatsbyでSCSSをMixinする方法を参考にしてください

TypeScriptの導入

  • 事前にReactとTypeScriptをいろんなサイトで勉強しました(2hぐらい)ので、Gatsbyに導入していきます
  • 以前はgatsby-plugin-typescriptをプラグインに導入する必要がありましたが、GatsbyのTypeScriptサポートがデフォルトになったようで、必要なくなりました
  • GraphQLのQuery型を自動で精製してくれるgatsby-plugin-graphql-codegenの導入
    • かなりつまったのが、Gatsbyの仕様でGraphQLで取得したデータはdataという引数で取得されるのですが、それがわかっておらずdataという引数をなぜpagesが持っているのだろうと思っていました
gatsby-config.js
module.exports = {
     --- (中略) ---
plugins: [
     --- (中略) ---
    {
      resolve: 'gatsby-plugin-graphql-codegen',
      options: {
        fileName: 'types/graphql-types.d.ts',
      },
    },
     --- (中略) ---
  • typescript用でScoped Styleを定義しようとすると、以下のエラー表示されます
header.tsx
import * as React from 'react'
import { Link } from 'gatsby'
// Cannot find module './header.module.scss' or its corresponding type declarations. という警告がLintから表示される
import * as styles from './header.module.scss'
  • そこでtsっぽさはなくなりますが、Lintの設定を変更しrequireで取り込むようにします
eslintrc.json
     --- (中略) ---
"rules": {
    "react/prop-types": "off",
    "no-var-requires": 0,
    "@typescript-eslint/no-var-requires": 0
  },
     --- (中略) ---
header.tsx
import * as React from 'react'
import { Link } from 'gatsby'
const styles = require('./header.module.scss')
  • pagesとcomponentsをTSXに書き換える、LintとPrettierの設定追加

各ページの作成

  • 上記の設定でやっと開発できるようになったので、各ページをモックを元に開発していきます
  • 以下が作っていてつまっていたところです.主にGatsby側の仕様になります

MicroCMSで空白で登録すると、取得できない

  • gatsby-source-microcmsプラグインを使っているのですが、なぜか空白がある項目に関してはtypes/graphql-types.d.tsへ出力されませんでした
  • いろいろ調べたのですが、どうしようもなかったので、空白がある場合は別テーブルにするということで対応しました
  • 他の解決策(とりあえずダミー項目を入れておく以外)を知っていたら教えていただきたいです

window.addEventListenerがBuildでエラー

  • Localでgatsby developしたときにはエラーにならないのですが、Netlifyでbuildするとエラーになりました。
10:12:05 PM: failed Building static HTML for pages - 4.129s
10:12:05 PM: error "window" is not available during server side rendering.
10:12:05 PM: 
10:12:05 PM:   14 | 
10:12:05 PM:   15 |   if (title === 'OVERVIEW') {
10:12:05 PM: > 16 |     window.addEventListener('scroll', (): void => {
10:12:05 PM:      |     ^
10:12:05 PM:   17 |       setIsTopScroll(window.scrollY === 0)
10:12:05 PM:   18 |     })
10:12:05 PM:   19 |   }
10:12:05 PM: 
10:12:05 PM:   WebpackError: ReferenceError: window is not defined
  • これはBuild時にSSGしており、windowオブジェクトなんかないよって行っているだと思います
  • 解決策としてはSSRのときに使いたいので、if文に`window !== 'undefined'を追加して対応しました
if (title === 'OVERVIEW' && typeof window !== 'undefined') {
  window.addEventListener('scroll', (): void => {
    setIsTopScroll(window.scrollY === 0)
  })
}

Netlifyにデプロイ

  • デプロイする前にローカルで確認してみます.以下の手順で問題なく画面が表示されることを確認してください(console側も)
$ yarn build
# public配下へ静的ファイルが出力される
$ yarn serve
# http://localhost:9000/ へ接続
  • 問題なければNetlifyの登録を実施します
  • 登録完了後、MyPageからNew site from Gitを選択 image.png
  • GitHubを選択し、認証します(今回はすべてのリポジトリを許可しています) image.png
  • 認証が完了すると、以下のようにリポジトリが選択できるので対象のリポジトリを選択します image.png
  • 特に問題ないので、そのままDeploy siteを選択します image.png
  • Deployがはじまります image.png
  • Deployが失敗しました.赤文字になっているSite deploy failedがリンクになっているので選択します image.png
  • ログを確認します.microCMSのAPI Keyが設定していないことが原因のようでした image.png image.png
  • Site Settings > Build & deploy > Environment > Environment variables > Edit VariablesでAPI Keyを設定します.Gastbyの設定で.envに設定したものと同じものですね image.png
  • Deploy logをみることができた場所からRetry deployができるので再デプロイします
  • Deployが成功しました.URLが表示されているので遷移して確認できます image.png image.png

(Option)独自ドメインの設定

  • こちらはお金が発生しますので、やりたい人だけ
  • 公開したポートフォリオサイトに独自ドメインをつけましょう

前提条件

  • Netlifyで公開できていること
  • お名前.comで独自ドメインを取得できていること

Netlify側の手順

  • Domain settingsを選択

image.png

  • Add custom domainを選択
    • まだ変更していなかったので、Options > Edit site nameからデフォルトのドメイン名も一応変えておきました

image.png

  • お名前.comで取得したドメインを入力してVerifyを入力

image.png

  • 「ドメイン名 already has an owner. Is it you?」と聞かれるので、「Yes, add domain」を選択

image.png

  • Custom domainsに追加したドメインが表示されます

image.png

  • Check DNS configurationを選択して、赤枠部分をメモしておきます

image.png

お名前.comの設定

  • 管理画面からドメイン設定タブを選択
  • ネームサーバーの設定からネームサーバーの変更を選択

image.png

  • ドメインを選択し、ネームサーバーの選択ではその他を選択し、先ほどメモしておいたネームサーバを入力します

image.png

  • NetlifyのCustom Domainで追加したドメインがNetlify Domaint表示されていることが確認できます

image.png

SSL化

  • SSL化はNetlifyが自動でやってくれます
  • HTTPSの方を見ると、Waiting on DNS propagationと表示されています

image.png

  • L5 ~ 10分ほど待てば以下のようにYour site has HTTPS enabledと表示されるはずです

image.png

最後に

よく3日でできるとかあるんですけど、実際仕事をしながら3日の時間を作り出すのって大変なんですよね。平日は疲れてるし、土日は休みたい(そもそも子どもの世話とかしてたら平日よりも大変だ)し。しかもだいたいこういうのやるときって勉強も兼ねているから自分の持っていない技術スタックを使いがちで、そうなると全然3日でできないじゃん!てなります笑
ただそうは言ってもなんとなく知っているのと、やったことがあるは全然違います!
とりあえず手を動かすことが大切だと思います。今日から一時間でもよいのでやってみましょう!

参考

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

ReactとFirebaseを使ってログインフォームを実装する④

今回でログインフォームは完成です!あとはFirebase側の処理だけです。

Google認証

Firebaseのコンソール画面でAuthenticationからログイン方法をクリック。Googleの認証を有効にします。

プロジェクトの公開名と、プロジェクトのサポートメールを設定して有効にするのチェックを入れます。そして保存をクリックしましょう。
react-firebase-login4-1-1-800x545.png
これでGoogle認証は完了です!簡単ですね。

Twitter認証

ツイッター認証はデベロッパー登録が必要です。これは申請が必要で、申請に必要な入力項目は時期によって異なります。
下記のQiitaの記事が詳しいです。認証に使うだけなら申請は問題ないと思います。

https://qiita.com/kngsym2018/items/2524d21455aac111cdee

登録できたら https://developer.twitter.com/ で新しいアプリを作成します。アプリのKeys and tokensから

  • Consumer API keys
  • Access token & access token secret

の二つをコピーしておきます。

react-firebase-login4-2-800x401.png

FIrebaseコンソールに移りTwitterの画面を開きます。記録したAPIキーとAPIシークレットを入力して有効にするをオンにし保存します 。そして今度は下にあるコールバックURLをコピーしておきましょう。

react-firebase-login4-3-800x399.png

ツイッターのアプリを開きCallback URLsにコピーしたURLを貼り付けます。

react-firebase-login4-4.png
これでTwitter認証は完了です!サイトのプライバシーポリシーのページがすでにあるなら、Twitterのアプリに登録しておくとよいでしょう。

Facebook認証

https://developers.facebook.com/ でアプリを追加します。

アプリのダッシュボードで設定>ベーシックをクリックしましょう。表示されたアプリIDapp secretをコピーしておきます。
react-firebase-login4-5-800x212.png
FIrebaseコンソールに移りFacebookの画面を開きます。アプリケーションIDとアプリシークレットを入力して有効にするをオンにして保存します 。Twitterと同じように下にあるコールバックURLをコピーしておきましょう。
react-firebase-login4-6-800x408.png
またFacebookに移りFacebookログイン>設定をクリックして有効なOAuthリダイレクトURIにコピーしたURLを貼り付けます。
react-firebase-login4-7-800x343.png
これでFacebook認証は完了です!Facebookのアプリは開発中になっているので、実際に使用するときはライブモードに切り替えましょう。

おわり

お疲れ様でした!今回でバックエンド側の処理も完了しログインフォームは完成です!!

ReactとFirebaseを使えばソーシャルログインも簡単に実装できます。自前でバックエンドを開発する場合も、アカウントの管理だけFirebaseを利用するのは有効な手段だと思います。

このチュートリアルでは全4回に渡ってUIの見た目と、ソーシャルログインや、ログイン後のリダイレクト処理について解説しました。

私のように個人や独学で勉強している人のお役に立てれば幸いです。

全4回

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

AtomでのJSX内コメントアウト方法

まずはじめに

ども〜
普段エディターは何使ってますか??
私はいろいろ手を出してるのですが、主に使っているのがAtomです。
ある日、普段通りAtomでReactを書いているときにJSXコード内のHTMLタグをコメントアウトしようと思いmacのcommand + /を押してみると

App.js
return(
  <div className="App">
    //<p>コメントアウトされたい</p>
  </div>
)

となり、JSXのコメントアウト記法である{/* */}で文字を囲めないのである。
VScoder「VScodeではパッケージインストールすれはできるけど?」
周りを見れば結構な割合でVScodeを使っているし(私の周りだけかな?)、実際使いやすい。
しかし私のエディターの使い初めがAtomで、どうも慣れから抜け出せない。
私「Atomがいい...Atomがいい(; ;)」

ということでタイトルの通りAtomでのJSXコメントアウト方法について備忘録ながら書く。
急いでいるときは2からみてね :coffee:

1 Atomのパッケージをインストール(失敗)

まずやったことがlangage-babelのインストールである。
Link : langage-babel
以下はlangage-babelパッケージに書いてある説明。

Commenting out JSX elements

JSX elements cannot be commented out by using standard // or /* / commenting. Normally {/ */} is used instead. language-babel changes the Atom toggle comments behaviour when inside a JSX block to support this behaviour. Nested elements within JSX that require a // form of commenting will be detected automatically.

JSXの要素は///* */でコメントアウトできないよね。
でもこのパッケージ入れればJSX内のコメントアウト!と検知してJSXにあったコメントアウト{/* */}を使えるよ〜とのこと。

うん。解決。なんで今まで探すのめんどくさがってて入れなかったんだろ。手打ちでめんどくさい思いともさよなら。

Atomにて
Preference -> install -> Search packagesで langage-babelと入力。
そしてインストールしてAtomを再起動。
JSXを記述しているファイルに行き、command + /
すると...

App.js
return(
  <div className="App">
    //<p>コメントアウトされたい</p>
  </div> 
)

... ^_^;
その後もこのパッケージについて少しみてみたがコメントアウトがショートカットでできそうにない。
しょうがないからVScodeにするか手打ちでやるか〜と思い(10回目)やっていたReactの勉強に戻る。
しかし、またすぐにコメントアウトする場面に出会い手打ちでやろうと思ったがこの手打ちめんどくさいな、と思うのが後何回続くのかと嫌になりもう一回だけ調べてみようと決心。次できなければGood Bye Atom また会う日まで。

そしてJSX commentoutで検索。情報の海へ。
すると以下にたどり着く。

2 keymap と initscriptに書き込む(成功)

Link : Comment out JSX code on Atom
にたどり着いた。
諦め癖のついているプログラマーには到底向いてない私はあまり期待せずに読んでみる。

# If you worked with React and JSX you probably noticed that you can't use JS comments when inside JSX sections
# Add this to your Atom init script
# Then add 'ctrl-cmd-/': 'comment-jsx' to your keymap.cson
# Then when you are on a JS/JSX file, just press cmd+ctrl+/ to use JSX-style comments that work with JSX elements
# Is not the most efficient way, but it's the cleanest and reliable one

なになに、React及びJSXでコメントアウトが正常にできないのに気づいたあなたはこれをいれればいいよ。とのこと。
ほう。期待してないけど手順の通りやってみよう。

1. まずAtomのInit Scriptを開いて以下を記入

init.coffee
atom.commands.add 'atom-workspace', 'comment-jsx', ->
  atom.config.set('editor.commentStart', '{/*', {scopeSelector: '.source.js.jsx'})
  atom.config.set('editor.commentEnd', '*/}', {scopeSelector: '.source.js.jsx'})
  for selection in atom.workspace.getActiveTextEditor().selections
    selection.toggleLineComments()
  atom.config.unset('editor.commentStart', {scopeSelector: '.source.js.jsx'})
  atom.config.unset('editor.commentEnd', {scopeSelector: '.source.js.jsx'})

2. 次はKeymapを開いて以下を記入

keymap.cson
'atom-workspace':
  'ctrl-cmd-/': 'comment-jsx'

3. 再起動

コメントアウトしたいファイルを開いてcommand + control + /を押す。

App.js
return(
  <div className="App">
    {/*<p>コメントアウトされたい</p>*/}
  </div>
)

...( ; ; )できた!!
やっとショートカットでコメントアウトできた。
諦めずに(?)探してよかったです。


ちなみに...Comment out JSX code on Atom のプログラマーさんたちのコメントで langage-babel は私も機能してないと2件ほどコメントされてたのでなんか間違ってるんでしょう( langage-babelの更新も2年前に止まってますし、bugsのコメントをみてみたらAtomのアップデートでの関係で使えなくなってるそうです。)

ちなみに、AtomにReact関係で入れているパッケージは

  • react : JSX内でHTMLの補完をしてくれるもの(これも2018年からアップデート止まってる)

  • platformio-ide-terminal : Atomでターミナル開けるやつ。Atomの左下にターミナル開くボタンがついてる。ターミナル開くのはVScodeに最初っからあるやつだけど..うんまあAtomがスキダカラ..

  • あとは特に入れた覚えないです。入れた方がいいものあれば教えてください。

3 終わりに

ようやくAtomでのReactのJSX内でコメントアウトできたのでよかったですv(^ ^)
日本語の記事探していても見つからず、やるのめんどくさいし辞めよかな〜と思っていましたが無事にAtomライフを続けられそうです。
日本語の記事が見つからないと同時にPC変えた時などの設定でわかりやすいようにここにまとめておきます。

最後に一言 : これをみてAtomを使ってる将来の自分へ、めんどくさがらずにすぐ調べ英語の記事を積極的に読もう!

それじゃ、ばいばい〜

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

Amplify + AppSync + React + Typescriptで簡単アプリ作成【完成】

概要

前回の記事の続きを書いていきます。
前回の記事をみたい人はこちら

クライアントからAPIを呼び出す

プロジェクト内でAppSyncを仕様するために, 提供されているライブラリを使用していきます。

$ yarn add aws-amplify aws-amplify-react

package.jsonを確認してインストールがされたことを確認してください。
確認できたら早速、エントリーポイントであるindex.tsxにインポートしましょう。

import './index.css';

import Amplify from 'aws-amplify'; // <--- ライブラリインポート
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';
import config from './aws-exports'; // <--- 追加
import * as serviceWorker from './serviceWorker';

// バックエンドの情報をAmplifyに渡してあげる
Amplify.configure(config);

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
serviceWorker.unregister();

一覧表示

最初の一歩として、一覧表示のAPIとつなぎ込んでみましょう。

import './App.css';

import { API, graphqlOperation } from 'aws-amplify';
import React, { useEffect, useState } from 'react';

import { listTodos } from './graphql/queries';

type Todo = {
  id: string;
  name: string;
  description: string | null;
  createdAt: string;
  updatedAt: string;
};

const App: React.FC = () => {
  // Todoリスト
  const [posts, setPosts] = useState<Todo[]>([]);

  useEffect(() => {
    (async () => {
      // Todoの一覧取得
      const result = await API.graphql(graphqlOperation(listTodos));
      // graphqlOperationの内容によって戻り値が変わるのと、objectで特に型の指定もできないで型ガード入れてキャストしている
      if ("data" in result && result.data) {
        const posts = result.data as ListTodosQuery;
        if (posts.listTodos) {
          console.log(posts.listTodos);
          setPosts(posts.listTodos.items as Todo[]);
        }
      }
    })();
  }, []);
  return (
    <div className="App">
    </div>
  );
};

export default App;

まだamplifyで連携しているDB(DynamoDB)にデータがないので、空の配列が取得できると思います。
デベロッパーツールのコンソールを確認してみてください。
確認ができたら接続はできているかと思います。

登録

一覧表示してもデータがなければ意味がないので、新規追加APIともつなぎ込みます。

import './App.css';

import { API, graphqlOperation } from 'aws-amplify';
import React, { useEffect, useState } from 'react';

import { ListTodosQuery, OnCreateTodoSubscription } from './api';
import { createTodo } from './graphql/mutations';
import { listTodos } from './graphql/queries';

type Todo = {
  id: string;
  name: string;
  description: string | null;
  createdAt: string;
  updatedAt: string;
};

const App: React.FC = () => {
  // Todoリスト
  const [posts, setPosts] = useState<Todo[]>([]);

  // Todo名
  const [name, setName] = useState("");

  // Todo内容
  const [description, setDescription] = useState("");

  useEffect(() => {
    (async () => {
      // Todoの一覧取得APIを呼ぶ
      const result = await API.graphql(graphqlOperation(listTodos));
      if ("data" in result && result.data) {
        const posts = result.data as ListTodosQuery;
        if (posts.listTodos) {
          setPosts(posts.listTodos.items as Todo[]);
        }
      }
    })();
  }, []);

  // Todoを新規追加
  const addTodo = async () => {
    if (!name || !description) {
      return;
    }
    // パラメタ
    const createTodoInput = {
      name,
      description,
    };

    try {
      //  Todoの新規追加APIを呼ぶ
      await API.graphql(
        graphqlOperation(createTodo, { input: createTodoInput })
      );
    } catch (error) {
      console.log(error);
    }
  };

  // Todo名の入力値をstateにセットする
  const handleChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  };

  // Todo内容の入力値をstateにセットする
  const handleChangeDescription = (e: React.ChangeEvent<HTMLInputElement>) => {
    setDescription(e.target.value);
  };

  return (
    <div className="App">
      <div>
        Todo名
        <input value={name} onChange={handleChangeName} />
      </div>
      <div>
        Todo内容
        <input value={description} onChange={handleChangeDescription} />
      </div>
      <button onClick={addTodo}>Todo追加</button>
    </div>
  );
};

export default App;


実際にフォームからTodoを追加してみましょう。
追加したらDynamoDBにレコードが追加されていることを確認してください。

そこまでできたら、あとは追加したTodoをリアルタイムで表示したいですよね。

サブスクリプション(購読)

Todoの追加をトリガーにリアルタイムで画面を更新した内容で描画するようにしてみます。

import './App.css';

import { API, graphqlOperation } from 'aws-amplify';
import React, { useEffect, useState } from 'react';

import { ListTodosQuery, OnCreateTodoSubscription } from './api';
import { createTodo } from './graphql/mutations';
import { listTodos } from './graphql/queries';
import { onCreateTodo } from './graphql/subscriptions';

type PostSubscriptionEvent = { value: { data: OnCreateTodoSubscription } };
type Todo = {
  id: string;
  name: string;
  description: string | null;
  createdAt: string;
  updatedAt: string;
};

const App: React.FC = () => {
  // Todoリスト
  const [posts, setPosts] = useState<Todo[]>([]);

  // Todo名
  const [name, setName] = useState("");

  // Todo内容
  const [description, setDescription] = useState("");

  useEffect(() => {
    (async () => {
      // Todoの一覧取得APIを呼ぶ
      const result = await API.graphql(graphqlOperation(listTodos));
      if ("data" in result && result.data) {
        const posts = result.data as ListTodosQuery;
        if (posts.listTodos) {
          setPosts(posts.listTodos.items as Todo[]);
        }
      }

      // 新規追加イベントの購読
      const client = API.graphql(graphqlOperation(onCreateTodo));
      if ("subscribe" in client) {
        client.subscribe({
          next: ({ value: { data } }: PostSubscriptionEvent) => {
            if (data.onCreateTodo) {
              const post: Todo = data.onCreateTodo;
              setPosts((prev) => [...prev, post]);
            }
          },
        });
      }
    })();
  }, []);

  // Todoを新規追加
  const addTodo = async () => {
    if (!name || !description) {
      return;
    }
    // パラメタ
    const createTodoInput = {
      name,
      description,
    };

    try {
      //  Todoの新規追加APIを呼ぶ
      await API.graphql(
        graphqlOperation(createTodo, { input: createTodoInput })
      );
    } catch (error) {
      console.log(error);
    }
  };

  // Todo名の入力値をstateにセットする
  const handleChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  }; 

  // Todo内容の入力値をstateにセットする
  const handleChangeDescription = (e: React.ChangeEvent<HTMLInputElement>) => {
    setDescription(e.target.value);
  };

  return (
    <div className="App">
      <div>
        Todo名
        <input value={name} onChange={handleChangeName} />
      </div>
      <div>
        Todo内容
        <input value={description} onChange={handleChangeDescription} />
      </div>
      <button onClick={addTodo}>追加</button>
      <div>
        {posts.map((data) => {
          return (
            <div key={data.id}>
              <h4>{data.name}</h4>
              <p>{data.description}</p>
            </div>
          );
        })}
      </div>
    </div>
  );
};

export default App;

追加アクションの監視を実装しました。
マウント時に一覧取得、新規追加の購読を行い、Todoが追加されると新規で追加したTodoが表示している一覧に追加され表示される流れです。
実際にアプリケーションを起動して試してみてください。

最後に

記事だけでみると一見難しそうな感じはしていましたが、いざ触ってみるとこんな簡単にGraphQLのAPIが作れてしまうのは驚きました。
今回はDynamoDBがメインで連携していましたが、他にもlambdaやcognitoとも連携ができるので時間あるときに試してみようかなと思います。

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