- 投稿日:2020-08-24T22:05:05+09:00
問い合わせを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.1my-portfolioという名前のプロジェクトが作成されました。
問い合わせフォームの作成
まずmaterial-uiのパッケージをインストール
お使いの作業ディレクトリで
$ npm install --save @material-ui/coreインストール!!
実装
TextInput.jsimport 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.jsimport { 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;Slackに通知を送る
Slackのワークスペース・チャンネルを開設
参考:Slackの使い方
- ワークスペース作成
右上の「ワークスペースを作成する」を押し、繊維策の画面でメールアドレスを入力します。
入力したアドレス宛に6桁のコードが届くのでそちらを入力すればワークスペースを開設できます。
できた!「New WorkSpace」横からWebアプリで設定をいじれます。
ワークスペース名、表示言語、URLを変えました。
- チャンネルの開設
必要な情報を入力しCreateをクリックでチャンネル開設です!
Incoming Webhookインテグレーションを追加
以下ページにアクセスし、post to channelで通知を送りたいチャンネルを選択します。
https://myportfolio-1995.slack.com/apps/new/A0F7XDUAZ-incoming-webhooksするとWebhook URLを取得できます。
ここで取得できるURLは明かさない方がいいでしょう!
処理の実装
SendNotificationToSlack.jsimport {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に通知を送ってみましょう!
あれれ、、ページにアクセスしただけで通知が送られてきちゃいました~?
renderごとに関数が実行されてフォームに1文字入力するごとに問い合わせが送られてくる迷惑極まりないバグ
コンポーネントのonClickに渡す関数が「functionName()」と()が付いていた為、renderごとに実行されてしまいました。
なので、ContactFormsコンポーネント内に以下の関数を作成し、
ボタンクリック→clickSendButton関数実行→SendNotificationToSlack関数実行という順番に処理を行うようにします。ContactForms.jsimport { 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;これで正常に動くはずです。
- 投稿日:2020-08-24T21:20:45+09:00
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します.
Home
でexact
を指定する理由私は,Homeで表示されるページに
exact
を指定すると勘違いしていましたが,上記のことを踏まえるとあながち間違えではないことが分かります.
つまり,<Route exact path="/" component={Home} />のように
exact
を指定してあげないと.すべてのlocation.path
についてHomeのpath
と部分一致してしまうからです.参考
- 投稿日:2020-08-24T18:36:19+09:00
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の順に確認する。
dataを開いて、中にデータが入っていれば完璧!最後に
openAPIを触ったことで、ロジック部分も学びました。しかし、理解が追いつかなかったため前提知識としてDDD、クリーンアーキテクチャの理解が必要だと感じました。
- 投稿日:2020-08-24T17:18:32+09:00
Next.jsのプリレンダリング方式についてまとめてみた
最近Next.jsを使い始めましたが、Next.jsにおいて重要な概念であるプリレンダリング方式(SSR・SSG)についてあまり理解できなかったため、公式ドキュメントを参考にそれぞれの方式の違いや使い分けについてまとめました。
この記事では、用語の説明にフォーカスしており、実装方法は記載してませんのでご了承ください。
プリレンダリングとは?
プリレンダリングとは、簡単にいうと事前にHTMLを生成することです。
通常のReactアプリケーション(SPA)の場合、ユーザーがWebページにアクセスし、Webページを表示する時にブラウザ側でHTMLを生成します。(クライアントサーバーレンダリング)
プリレンダリングでは、ユーザーがアクセスする前に事前にHTMLを生成し、その用意されたHTMLをユーザーに提供する方式となっています。そのため、ブラウザの負荷を下げて表示を高速化することができます。
また、事前に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を生成するため、常に最新の状態をユーザーに見せることができます。
SSG(Static Site Generator)
SSGでは、アプリビルド時にHTMLを生成します。
リクエストごとにHTMLを生成せず、事前にビルドされたHTMLを再利用する形となるため、SSRよりもさらに高速な表示が可能です。
プリレンダリング方式(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を選択するのが最適かと思います。
参考資料
- 投稿日:2020-08-24T16:33:59+09:00
ReactのDropzoneでアップロードした画像をDjangoのOpenCVで顔検出して返すメモ
背景
大昔にドラッグアンドドロップでブラウザに画像を放り込むと
PHPで顔を検出して画像を返すシステムを作っていたので
それをReactとDjangoでやりたい方法論
なんかReactではDropzoneってのがあるらしいのでそれを使う
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.pyALLOWED_HOSTS = ["127.0.0.1"]なお私は127.0.0.1でやってない
server/setting.pyfrom django.contrib import admin from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('image/', include('image.urls')), ]image/urls.pyfrom 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.jsimport 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.jsrenderDropzone() { 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.jshandleOnDrop(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に失敗したメッセージが出てくる
エラーを無視すると図のような感じになるはず
以前と同様に
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.pyfrom 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.pydef 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.jssubmitData() { 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(); }); }結果
結論
参考文献整理したら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日
- 投稿日:2020-08-24T15:31:35+09:00
【鬼滅の刃】〜今日の呼吸ガチャ〜 を作りました
鬼滅の刃、面白いですね。
最近漫画とアニメを見終わったのですが、
ハマり過ぎて、
【鬼滅の刃 〜今日の呼吸ガチャ〜】
を作ってしまいました笑
今日の呼吸ガチャを回してみる↓
kimetsu-gacha.firebaseapp.comガチャを回すと、
今日のあなたの呼吸がわかります。使った技術
・React.js
・TypeScript
・Firebaseかかった時間
1日
なぜ作ったのか?
鬼滅の刃に猛烈にハマっていた
ReactとTypeScriptを勉強したくて、画面だけで完結するものを作りたかった
@twtjudy1128 さんの、プルジャダイニング(本格ネパール料理店)menuガチャからインスパイアされた
「ガチャなら画面だけでいけるやん!」で作ることを決定
参考にしたWEBサービス
過去に作ったアプリ
Qiitaの新着記事を、ニュース感覚で聞き流せるwebアプリ
聞い太?作って感じたことを一言で
「好きこそものの上手なれ」
作るのは楽しい!
- 投稿日:2020-08-24T15:28:27+09:00
【鬼滅の刃】〜今日の呼吸ガチャ〜 を作りました
鬼滅の刃、面白いですね。
最近漫画とアニメを見終わったのですが、
ハマり過ぎて、
【鬼滅の刃 〜今日の呼吸ガチャ〜】
を作ってしまいました笑
今日の呼吸ガチャを回してみる↓
kimetsu-gacha.firebaseapp.comガチャを回すと、
今日のあなたの呼吸がわかります。使った技術
・React.js
・TypeScript
・Firebaseかかった時間
1日
なぜ作ったのか?
鬼滅の刃に猛烈にハマっていた
ReactとTypeScriptを勉強したくて、画面だけで完結するものを作りたかった
@twtjudy1128 さんの、プルジャダイニング(本格ネパール料理店)menuガチャからインスパイアされた
「ガチャなら画面だけでいけるやん!」で作ることを決定
参考にしたWEBサービス
作って感じたことを一言で
「好きこそものの上手なれ」
作るのは楽しい!
- 投稿日:2020-08-24T15:13:05+09:00
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.tsimport { 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軽くしたいアルヨ……?♀️
- 投稿日:2020-08-24T12:22:58+09:00
Reactでカスタムフックを作ってAPIを共通化する方法
最初はEndPointごとにAPIアクセス処理を作成しようとしていた
サーバ側のAPIでデータを取得しようとするときに、API毎にAPIアクセス処理を作成しようとしていました。下記のような感じです。
下記の例は、News情報を
https://example.com/api/news
に取得し一覧に表示します。もしエラーが出たらerror
ページに遷移させるというものです。news.tsximport { 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.tsimport 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.tstype 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.tsimport 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.tsximport 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> ); };
- 投稿日:2020-08-24T11:52:08+09:00
React + SimpleBar: スクロールバーのスタイルをカスタマイズする
SimpleBarはスクロールバーをカスタマイズするライブラリです。スクロールバーを独自につくるのではなく、CSSのスタイルを割り当てるので、おかしな挙動は起こらず、ネイティブなスクロールのパフォーマンスが保たれます。あくまで、スクロールバーの見栄えを変えるだけです。
デザインは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■ヘッダと左カラムにメインコンテンツで組み立てられたページ
以下のコードは、アプリケーション(
src/App.js
)に、それぞれヘッダ(src/components/Header.js
)と左カラム(src/components/LeftColumn.js
)およびメインコンテンツ(src/components/MainContents.js
)を静的にレイアウトしたモジュールの中身です。src/App.js
がsimplebar/dist/simplebar.min.css
を読み込んでいます。SimplebarReactのJavaScriptライブラリを
import
するのは、スクロールバーをカスタマイズする左カラムのモジュール(src/components/LeftColumn.js
)です。なお、リストの連番項目は、メソッドArray.from()
とArray.prototype.map()
でつくりました。興味のある方は「ECMAScript 6のArrayに関わる構文を試す」をお読みください。src/App.jsimport 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.jsimport 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.jsimport 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.jsimport 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■ページ上部をヘッダが覆ってしまう
<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.jsimport 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.jsfunction 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のスクロールバーが自動表示される
ページ全体をスクロールしたときの不具合を直す
まだ少し、不具合が残っています。ページ全体を下にスクロールしたとき、左カラムがせり上がって、ヘッダにかぶってしまうことです(図004)。
図004■ページを下にスクロールすると左カラムがヘッダにかぶる
左カラム(
src/components/LeftColumn.js
)の垂直位置は固定しなければなりません。やり方は、前述「ヘッダを上部に固定する」と同じです。ただ、CSS(src/App.css
)でなく、style
属性で定めることにしました。src/App.jsfunction 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■メインコンテンツが左カラムに隠れてしまう
「ヘッダを上部に固定する」と同じ考え方で、メインコンテンツの左端をカラムの幅だけ右に寄せればよいはずです。けれど、つぎのコードではメインコンテンツの位置がまったく動きません。
src/App.jsfunction 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.jsconst 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■メインコンテンツの位置が正しく定まってスライダはグラデーションになった
冒頭の標準JavaScriptのサンプルと同じページをSimplebarReactでつくり、CodeSandboxに掲げました。また、Githubでもソースをご覧いただけます。
- 投稿日:2020-08-24T09:54:14+09:00
【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.tsximport 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;次にuseEffectでconsoleにログを出してみます。
App.tsximport 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])useStateとuseEffectは複数使用可能
App.tsximport 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;番外編
初期値の切り出し
Typescriptっぽく初期値を切り出してみました。
App.tsximport 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.tsximport 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の使い方をデモしてみました。
- 投稿日:2020-08-24T09:19:34+09:00
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のバージョンが古すぎてあまり参考になりませんでした。
XDのチュートリアルやってるけど、クソすぎてやばい。
— n_koyama (@naoto324) July 28, 2020
動画で進んでくんだけど
・完成イメージを見てみましょうとあるのに、完成イメージをDLするリンクがない
・アセットを作りましょうとあるけど、アセットがない(今はコンポーネントらしい)- ちづみさんのサイトが参考になりました
クロームの拡張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フロントでどんな技術を使うか考察
- フロントでやりたいことは以下
- 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みたいなイメージ
microCMS触ってるけどなんやこれ。。。
— n_koyama (@naoto324) August 8, 2020
めちゃくちゃ便利やん。
DB + APIみたいなイメージだけどバックエンドが隠蔽されていてちょっとめんどくさい画像も簡単に登録できるし、ええやん- アカウント登録
- サービス情報を入力.今回はプロフィールサイトを作りたいので以下で登録しました
- サービス名: [名前]のプロフィール
- サービスID: profile-koyama.microcms.io
- APIを追加
- データを登録
- 以下のHOBBYのような複数の項目を登録したい場合はAPIの型をリスト形式で作ればOKです
![]()
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/ に接続します
- 今後のために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.lockmicroCMSとの連携
- プラグインの追加
$ yarn add gatsby-source-microcms
- gatsby-config.jsの設定の修正
gatsby-config.jsplugins: [ `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に接続
API KeyがGitリポジトリ上で確認できるのはまずいので、環境変数として持っておきます.ローカルとNetlify両方いずれも対応させるにはプレフィックスに
GATSBY_
をつける必要があるようです".env"GATSBY_MICRO_CMS_API_KEY=********-****-****-****-********e8b9gatsby-config.jsrequire("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-sassgatsby-config.jsmodule.exports = { --- (中略) --- plugins: [ `gatsby-plugin-sass`, // 追加 --- (中略) ---
- 試しにindex.jsでのみに適用するSCSSを作成します.ページ名に合わせてindex.module.scssとします
index.module.scss.hoge { color: green; }
- index.jsで読み込みます
index.jsimport 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
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.jsmodule.exports = { --- (中略) --- plugins: [ --- (中略) --- { resolve: 'gatsby-plugin-graphql-codegen', options: { fileName: 'types/graphql-types.d.ts', }, }, --- (中略) ---
- typescript用でScoped Styleを定義しようとすると、以下のエラー表示されます
header.tsximport * 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.tsximport * as React from 'react' import { Link } from 'gatsby' const styles = require('./header.module.scss')
- pagesとcomponentsをTSXに書き換える、LintとPrettierの設定追加
- こちらのコミットログ参照
各ページの作成
- 上記の設定でやっと開発できるようになったので、各ページをモックを元に開発していきます
- 私はその前にNetlifyにデプロイ
- 以下が作っていてつまっていたところです.主に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を選択
![]()
- GitHubを選択し、認証します(今回はすべてのリポジトリを許可しています)
![]()
- 認証が完了すると、以下のようにリポジトリが選択できるので対象のリポジトリを選択します
![]()
- 特に問題ないので、そのままDeploy siteを選択します
![]()
- Deployがはじまります
![]()
- Deployが失敗しました.赤文字になっているSite deploy failedがリンクになっているので選択します
![]()
- ログを確認します.microCMSのAPI Keyが設定していないことが原因のようでした
![]()
![]()
- Site Settings > Build & deploy > Environment > Environment variables > Edit VariablesでAPI Keyを設定します.Gastbyの設定で.envに設定したものと同じものですね
![]()
- Deploy logをみることができた場所からRetry deployができるので再デプロイします
- Deployが成功しました.URLが表示されているので遷移して確認できます
![]()
![]()
(Option)独自ドメインの設定
- こちらはお金が発生しますので、やりたい人だけ
- 公開したポートフォリオサイトに独自ドメインをつけましょう
前提条件
- Netlifyで公開できていること
- お名前.comで独自ドメインを取得できていること
Netlify側の手順
- Domain settingsを選択
- Add custom domainを選択
- まだ変更していなかったので、Options > Edit site nameからデフォルトのドメイン名も一応変えておきました
- お名前.comで取得したドメインを入力してVerifyを入力
- 「ドメイン名 already has an owner. Is it you?」と聞かれるので、「Yes, add domain」を選択
- Custom domainsに追加したドメインが表示されます
- Check DNS configurationを選択して、赤枠部分をメモしておきます
お名前.comの設定
- 管理画面からドメイン設定タブを選択
- ネームサーバーの設定からネームサーバーの変更を選択
- ドメインを選択し、ネームサーバーの選択ではその他を選択し、先ほどメモしておいたネームサーバを入力します
- NetlifyのCustom Domainで追加したドメインがNetlify Domaint表示されていることが確認できます
SSL化
- SSL化はNetlifyが自動でやってくれます
- HTTPSの方を見ると、Waiting on DNS propagationと表示されています
- L5 ~ 10分ほど待てば以下のようにYour site has HTTPS enabledと表示されるはずです
最後に
よく3日でできるとかあるんですけど、実際仕事をしながら3日の時間を作り出すのって大変なんですよね。平日は疲れてるし、土日は休みたい(そもそも子どもの世話とかしてたら平日よりも大変だ)し。しかもだいたいこういうのやるときって勉強も兼ねているから自分の持っていない技術スタックを使いがちで、そうなると全然3日でできないじゃん!てなります笑
ただそうは言ってもなんとなく知っているのと、やったことがあるは全然違います!
とりあえず手を動かすことが大切だと思います。今日から一時間でもよいのでやってみましょう!参考
- 投稿日:2020-08-24T08:06:58+09:00
ReactとFirebaseを使ってログインフォームを実装する④
今回でログインフォームは完成です!あとはFirebase側の処理だけです。
Google認証
Firebaseのコンソール画面でAuthenticationからログイン方法をクリック。Googleの認証を有効にします。
プロジェクトの公開名と、プロジェクトのサポートメールを設定して有効にするのチェックを入れます。そして保存をクリックしましょう。
これでGoogle認証は完了です!簡単ですね。Twitter認証
ツイッター認証はデベロッパー登録が必要です。これは申請が必要で、申請に必要な入力項目は時期によって異なります。
下記のQiitaの記事が詳しいです。認証に使うだけなら申請は問題ないと思います。https://qiita.com/kngsym2018/items/2524d21455aac111cdee
登録できたら https://developer.twitter.com/ で新しいアプリを作成します。アプリのKeys and tokensから
- Consumer API keys
- Access token & access token secret
の二つをコピーしておきます。
FIrebaseコンソールに移りTwitterの画面を開きます。記録したAPIキーとAPIシークレットを入力して有効にするをオンにし保存します 。そして今度は下にあるコールバックURLをコピーしておきましょう。
ツイッターのアプリを開きCallback URLsにコピーしたURLを貼り付けます。
これでTwitter認証は完了です!サイトのプライバシーポリシーのページがすでにあるなら、Twitterのアプリに登録しておくとよいでしょう。Facebook認証
https://developers.facebook.com/ でアプリを追加します。
アプリのダッシュボードで設定>ベーシックをクリックしましょう。表示されたアプリIDとapp secretをコピーしておきます。
FIrebaseコンソールに移りFacebookの画面を開きます。アプリケーションIDとアプリシークレットを入力して有効にするをオンにして保存します 。Twitterと同じように下にあるコールバックURLをコピーしておきましょう。
またFacebookに移りFacebookログイン>設定をクリックして有効なOAuthリダイレクトURIにコピーしたURLを貼り付けます。
これでFacebook認証は完了です!Facebookのアプリは開発中になっているので、実際に使用するときはライブモードに切り替えましょう。おわり
お疲れ様でした!今回でバックエンド側の処理も完了しログインフォームは完成です!!
ReactとFirebaseを使えばソーシャルログインも簡単に実装できます。自前でバックエンドを開発する場合も、アカウントの管理だけFirebaseを利用するのは有効な手段だと思います。
このチュートリアルでは全4回に渡ってUIの見た目と、ソーシャルログインや、ログイン後のリダイレクト処理について解説しました。
私のように個人や独学で勉強している人のお役に立てれば幸いです。
全4回
- 投稿日:2020-08-24T01:16:24+09:00
AtomでのJSX内コメントアウト方法
まずはじめに
ども〜
普段エディターは何使ってますか??
私はいろいろ手を出してるのですが、主に使っているのがAtomです。
ある日、普段通りAtomでReactを書いているときにJSXコード内のHTMLタグをコメントアウトしようと思いmacのcommand + /
を押してみるとApp.jsreturn( <div className="App"> //<p>コメントアウトされたい</p> </div> )となり、JSXのコメントアウト記法である
{/* */}
で文字を囲めないのである。
VScoder「VScodeではパッケージインストールすれはできるけど?」
周りを見れば結構な割合でVScodeを使っているし(私の周りだけかな?)、実際使いやすい。
しかし私のエディターの使い初めがAtomで、どうも慣れから抜け出せない。
私「Atomがいい...Atomがいい(; ;)」ということでタイトルの通りAtomでのJSXコメントアウト方法について備忘録ながら書く。
急いでいるときは2からみてね
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.jsreturn( <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.coffeeatom.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.jsreturn( <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を使ってる将来の自分へ、めんどくさがらずにすぐ調べ英語の記事を積極的に読もう!
それじゃ、ばいばい〜
- 投稿日:2020-08-24T01:12:02+09:00
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とも連携ができるので時間あるときに試してみようかなと思います。