20200913のReactに関する記事は13件です。

Gatsby.jsで作ったブログにArchiveページを作成する

動機

静的サイトジェネレーターのGatsby.jsでブログを開発するにあたり、Archiveを作ろうと思い挑戦。
私が調べた(対して調べてない・・・)範囲ではArchive作る便利なpluginとかないないみたい。なので、実作。ベストプラクティスではない、と思う。

作り

記事を作成した月ごとにまとめるよくある感じにする。
まだ7、8月しか記事を書いてないので2ヶ月分しかない。もっと書かなきゃ。

環境

・OS: macOS 10.15.5
・gatsby: 2.24.14
・ヘッドレスCMS: Contentful
・FW: React Bootstrap

手順

1.templateフォルダにarchiveページ用のテンプレートを作成する。
2.gatsby-node.jsで、archiveページを生成する
3.archiveページのComponentを作成する。

まずはarchiveページ用のテンプレートページを作っていきます。テンプレートはページの構成が一緒で文章と画像を差し替えるだけでいいページをたくさん作る時に使います。便利。

archiveTemplate.js
import React from "react"
import Layout from "../components/layout"
import { graphql, Link } from "gatsby"
import { Container, Row, Col, Card, Button } from "react-bootstrap"
import Img from "gatsby-image"
import Archive from "../components/archive"

export const query = graphql`
  query($skip: Int!, $limit: Int!, $currentYearMonth: Date!, $nextYearMonth: Date!){
    allContentfulCode(
      sort: {fields: createdDate, order: DESC},
      limit: $limit,
      skip: $skip,
      filter:  {createdDate :
        {
          lt: $nextYearMonth,
          gte: $currentYearMonth
        }
      }
      ){
      edges {
        node {
          title
          slug
          category {
            category
            categorySlug
            id
          }
          description {
            content {
              content {
                value
              }
            }
          }
          createdDate(formatString: "YYYY/MM/DD")
          thumbnail{
            fluid(maxWidth: 500) {
              src
              ...GatsbyContentfulFluid_withWebp
            }
            description
          }
        }
      }
    }
  }
`

function ArchiveTemplate(props) {
  return (
    <Layout>
      <Container fluid >
        <Row className="justify-content-md-center">
          <Col xs={7} className="mt-3" style={{ borderBottom: "3px solid grey" }} >
            <h1 className="m-3">{`${props.pageContext.currentYearMonth}`}</h1>
          </Col>
          <Col xs={3} >
          </Col>
        </Row>
        <Row className="justify-content-md-center">
          <Col xs={7} style={{ padding: "20px", textAlign: "center" }} className="mt-3" >
            {props.data.allContentfulCode.edges.map((edge, index) => (
              <Card className="m-5">
                <Card.Body>
                  <Card.Text>
                    <Link to={`/code/${edge.node.category[0].categorySlug}/${edge.node.slug}`}>
                      <h3>{edge.node.title}</h3>
                    </Link>
                    {edge.node.category.map(x => (
                      <li className={x.categorySlug} key={x.id} style={{ listStyleType: "none" }}>
                        <Link to={`/code/${x.categorySlug}/`}>{x.category}</Link>
                      </li>
                    ))}
                  </Card.Text>
                </Card.Body>
                <figure>
                  <Link to={`/code/${edge.node.category[0].categorySlug}/${edge.node.slug}`}>
                    <Img
                      fluid={edge.node.thumbnail.fluid}
                      alt={edge.node.thumbnail.description}
                      style={{ height: "300px" }}
                    />
                  </Link>
                </figure>
                <Card.Body>
                  <Card.Text>
                    {edge.node.description.content[0].content[0].value}
                  </Card.Text>
                  <Button variant="outline-secondary" href={`/code/${edge.node.category[0].categorySlug}/${edge.node.slug}`}>See More</Button>
                </Card.Body>
                <Card.Footer className="text-muted">作成日 {edge.node.createdDate}</Card.Footer>
              </Card>
            ))}
            <div>
              {!props.pageContext.isFirst && (
                <div className="text-left" style={{ display: "inline" }}>
                  <Link
                    to={
                      props.pageContext.currentPage === 2
                        ? `/${props.pageContext.currentYear}/${props.pageContext.currentMonth}/`
                        : `/${props.pageContext.currentYear}/${props.pageContext.currentMonth}/${props.pageContext.currentPage - 1}`
                    }
                    rel='prev'>
                    <span className="m-5">前のページ </span>
                  </Link>
                </div>
              )}
              {!props.pageContext.isLast && (
                <div className="text-right" style={{ display: "inline" }}>
                  <Link to={`/${props.pageContext.currentYear}/${props.pageContext.currentMonth}/${props.pageContext.currentPage + 1}/`} rel='next'>
                    <span className="m-5">次のページ</span>
                  </Link>
                </div>
              )}
            </div>
          </Col>
          <Col xs={3}>
            <Archive />
          </Col>
        </Row>
      </Container>
    </Layout >
  )
}

export default ArchiveTemplate

ポイント

GraphQLのfilterを使って、データの絞り込みを行う。今回は対象の月のデータを、対象の月以上、翌月未満で絞り込んで取得。
filter: {createdDate :
{
lt: $nextYearMonth,
gte: $currentYearMonth
}
}

次にarchiveページを生成します。生成場所は、gatsby-node.jsです。
まずはqraphqlを使って、全データを取得します。項目は記事生成日のみ。

gatsby-node.js
const path = require('path')

module.exports.createPages = async ({ graphql, actions}) => {
  const { createPage } = actions
  const archiveTemplate = path.resolve('./src/template/archiveTemplate.js')
  const res = await graphql(`
    query{
      allContentfulData(sort: {fields: createdDate, order: DESC}) {
        edges {
          node {
            createdDate
          }
        }
      }
    }
  `)

次にarchiveページを生成していきます。

gatsby-node.js
// archive //
  const archivePerpage = 5 //1ページに表示する記事の数
  const yearMonth = ['2020-07', '2020-08', '2020-09', '2020-10', '2020-11', '2020-12'];
  const archiveByMonth = yearMonth.map(x => {
    const obj = {};
    obj[x] = (res.data.allContentfulData.edges.filter(y => y.node.createdDate.match(x))).length;
    return obj;
  });

  archiveByMonth.forEach((x) => {
    const archivePosts = x[Object.keys(x)] //記事の総数 
    if (archivePosts === 0) return;
    const archivePages = Math.ceil(archivePosts / archivePerpage) //記事一覧ページの総数
    const currentYear = Object.keys(x)[0].split('-')[0];
    const currentMonth = Object.keys(x)[0].split('-')[1];
    const nextMonth = Number(currentMonth) + 1 === 13 ? '01' : ('00' + String(Number(currentMonth) + 1)).slice(-2);
    const nextYear = (nextMonth === '01') ? String(Number(nextMonth) + 1) : currentYear;

    Array.from({ length: archivePages }).forEach((_, i) => {
      createPage({
        path:
          i === 0
            ? `/${currentYear}/${currentMonth}`
            : `/${currentYear}/${currentMonth}/${i + 1}/`,
        component: archiveTemplate,
        context: {
          currentYearMonth: currentYear + '-' + currentMonth,
          nextYearMonth: nextYear + '-' + nextMonth,
          currentYear,
          currentMonth,
          skip: archivePerpage * i,
          limit: archivePerpage,
          currentPage: i + 1, // 現在のページ番号
          isFirst: i + 1 === 1, //最初のページ
          isLast: i + 1 === archivePages, // 最後のページ
        }
      })
    })
  })

ポイント

月ごとにデータ数をカウントする。
const yearMonth = ['2020-07', '2020-08', '2020-09', '2020-10', '2020-11', '2020-12'];
  const archiveByMonth = yearMonth.map(x => {
const obj = {};
obj[x] = (res.data.allContentfulData.edges.filter(y => y.node.createdDate.match(x))).length;
return obj;

URLは/YYYY/MM/ページ番号にする。
? `/${currentYear}/${currentMonth}`
: `/${currentYear}/${currentMonth}/${i + 1}/`,

最後にarchiveのCompenentを作成する。

archive.js
import React from 'react';
import { useStaticQuery, graphql, Link } from 'gatsby'
import { Container, Row, Col } from 'react-bootstrap';
import Style from './layout.module.scss'

const Archive = (props) => {
    const datas = useStaticQuery(graphql`
    query{
        allContentfulCode {
            edges {
              node {
                createdDate(formatString:"YYYY/MM/DD")
               }
            }
        }
    }
`)
    const date = ['2020/07', '2020/08', '2020/09', '2020/10', '2020/11', '2020/12'];
    const count = date.map(x => {
        const obj = {};
        obj[x] = (datas.allContentfulCode.edges.filter(y => y.node.createdDate.match(x))).length;
        return obj;
    });
    return (
        <div className={Style.archive_wrap}>
            <Container className={Style.content}>
                <Row>
                    <Col>
                        <h3>Archive</h3>
                        <ui>
                            {count.map((x, index) => {
                                if (x[Object.keys(x)] === 0) return false;
                                return <li key={index}><Link to={`/${Object.keys(x)}`}>{`${Object.keys(x) + ' (' + x[Object.keys(x)] + ')'}`} </Link> </li>
                            }
                            )}
                        </ui>
                    </Col>
                </Row>
            </Container>
        </div>
    );
}

export default Archive

ポイント

記事数が0の月はarchiveリストに表示しない。
if (x[Object.keys(x)] === 0) return false;

完成

https://sakublog.netlify.app/

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

Firebase Hosting 環境構築~デプロイ

環境構築からデプロイまで行った作業記録です。
躓いたところをメモっています。
以下を参考にさせていただきました。

プロジェクトの作成

ブラウザからFirebaseへアクセスしログイン、プロジェクトを作成する。

image.png

image.png

firebase-tools のインストール

npm install -g firebase-tools

create-react-app のインストール

npm install -g create-react-app

これをしないと、create-react-app実行時にエラーが出る。

create-react-app : 用語 'create-react-app' は、コマンドレット、関数、スクリプト ファイル、または操作可能なプログラムの名前として認識されません。

ローカルでプロジェクトの作成

mkdir react-app

cd react-app

create-react-app hello-world

We suggest that you begin by typing:

  cd hello-world
  npm start

Happy hacking!

ローカルで起動

App.jsを修正

image.png

$ npm start

image.png

Firebaseとの連携

firebase login

login.png

ログイン後

successful.png

コンソールに以下が表示される。

Waiting for authentication...

+  Success! Logged in as XXXXX@gmail.com

Firebase Hostingへの接続設定を追加

$ firebase init

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:
省略
? Are you ready to proceed? Yes
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection)
 ( ) Database: Deploy Firebase Realtime Database Rules
 ( ) Firestore: Deploy rules and create indexes for Firestore
 ( ) Functions: Configure and deploy Cloud Functions
>( ) Hosting: Configure and deploy Firebase Hosting sites
 ( ) Storage: Deploy Cloud Storage security rules
 ( ) Emulators: Set up local emulators for Firebase features

エラーが表示。スペースを押してなかったため。

Error: Must select at least one feature. Use SPACEBAR to select features, or provide a feature with firebase init [feature_name]

再度実行しHostingを選択、スペース を押しエンター。

Use an existing projectを選択し作成したプロジェクトを選択。

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: 
> hello-world-9b4bd (hello-world)

※後述するがFile dist\index.html already exists. Overwrite?の選択肢はNoにすること。

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
+  Wrote public/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

+  Firebase initialization complete!

Configure as a single-page app (rewrite all urls to /index.html)? の選択肢の違いはこちら。
firebase init 時に聞かれる「Configure as a single-page app (rewrite all urls to /index.html)?」は選択によって何がどう変わるのか

Firebaseへのデプロイ

npm run build

firebase deploy

デプロイ後以下の画面になる。
数分待てばページが更新されるという記事もあったが更新されない。

image.png

原因

こちらの記事より
【Firebase】Firebase Hosting Setup Completeと表示されてしまう

firebase init した時の選択肢で、
File dist\index.html already exists. Overwrite? No
ここ、絶対Noにすること!

削除してやり直しした。

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
? File public/index.html already exists. Overwrite? No
i  Skipping write of public/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

+  Firebase initialization complete!

今度は成功した。

image.png

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

React Hooksで作るFlux入門

この記事について

モダンフロントエンドにおいて、Fluxというアプリケーションアーキテクチャが存在します。
従来、Fluxの思想に従って実装を行うためには、同名ライブラリ"Flux"やReduxが採用されるケースが多かったのですが、React16.8でのReact Hooksの登場により、ライブラリに頼ることなくFluxを実現できるようになりました。
本記事では、Fluxの概念・なぜそれが必要なのかについて説明した後、React Hooksを用いたFlux実装の一例を紹介します。

使用する環境・バージョン

  • OS: macOS Mojave 10.14.5
  • Node.js: v12.13.0
  • npm: 6.13.4
  • React: 16.13.0

読者に要求する前提知識

JS, React, React Hooksが書けるだけの知識があること

Fluxとは?

Fluxとは、UIをもつwebアプリケーションを構築するときのデザインパターン/アーキテクチャです。

アプリケーションのデザインパターンといえば、他にはMVC(Model View Controller)パターンやMVVM(Model-View-ViewModel)などが存在します。
Fluxもそれらと同様に、「アプリケーションを作るときに、どういう構造にするべきなのか」という考え方の一つなのです。

Fluxが誕生した背景

MVCやMVVMがある中で、なぜFluxという思想が新しく生まれたのでしょうか。その疑問に答えるためには、背景を見ていきます。

レガシーアプリケーションの画面描画の仕組み

従来のレガシーなWebアプリケーションというのは、以下のようなスタイルでコンテンツを生成していました。
1. クライアントがHTTPリクエストを送信
2. サーバーサイドで、リクエストに応じたHTMLを生成→送信(必要ならばAPI・DBなどからのデータ取得を行う)
3. クライアントは、サーバーから受け取ったHTMLをそのまま表示

このシステムでの特徴としては、「新しい画面・コンテンツの表示には、サーバーから新しく画面ファイルの取得→画面のリロードが必要」ということです。
1.png

しかし、これではちょっとした画面更新だけで、HTMLやCSS,JSファイルをいちいちやりとりしなければいけないので、応答速度が落ちるという欠点がありました。

SPA(Single-Page Application)の画面描画の仕組み

ここでSPA(Single-Page Application)というものが登場しました。これは、画面更新の際に
1. クライアントがサーバーに画面更新に必要なデータを要求
2. サーバーサイドは、画面のhtmlファイルではなく、要求されたデータのみをjson等で返す
3. クライアントは、サーバーから受け取ったデータをもとに画面の一部を再レンダリング

という風にして、画面更新時のクライアントーサーバー間のやりとりを軽量にし、パフォーマンスを向上させています。
2.png

SPAの登場によって、フロントエンドの役割が大きく転換することになります。
レガシーアプリケーションでは、JSはサーバーサイドから受け取った画面のみを扱えばよかったのに対し、SPAでは画面更新時にどう正しく更新するかというところまでカバーしなくてはならないからです。

(おまけ:ちなみに、ページ生成という仕事をフロント側に任せることができるようになったため、バックエンド側の役割も変化しています。フロントエンドがデータ取得に使うためだけのAPI・マイクロサービスが多く造られるようになり、それらがBFF(Backends For Frontends)と呼ばれるようになりました)

参考:今さら聞けない!シングルページアプリケーションとは
参考:SPAにおける状態管理: 関数型のアプローチも取り入れるフロントエンド系アーキテクチャの変遷

SPAの再レンダリング時に、正しく画面を更新するために必要な考え方が「状態管理」です。

SPAの状態管理について

状態管理についてはこちらの記事が非常にわかりやすいため、これに沿って説明します。
参考:今から始めるReact入門 〜 flux編

例えば、「未読1件」の表示を画面にしているときに、新たにサーバーサイドから追加の「未読2件」の通知がきたとします。
このときフロントエンド側では、「今ある未読1件と、新たに追加された未読2件を合わせて、合計3件の未読がある」という風に判断し、表示しなければいけません。このように、「今の状態(=未読)」をずっと保持し、正しく更新し続ける機能のことを状態管理といいます。
そのときに、状態管理がされていないと、サーバーサイドからきた未読「2件」という数字をそのまま表示することになってしまいます。

Fluxで状態管理を行う流れ

Fluxはこの状態管理をReactでやりやすくしてくれます。

Fluxは以下の4つの要素で構成されています。

  • View: フォームやボタンといった、アプリケーションの画面
  • Action: アプリケーションの更新情報を得る為の内部API
  • Dispatcher: Actionを受け取って、アプリケーションの更新作業を実際に行う関数
  • Store: アプリケーションの状態の保持を行うデータストア

状態管理の流れとしては、
1. Viewで(クリック等の操作を行って)どんな更新をしたいのか、Actionに通知
2. ActionはViewからの通知を受け取って、dispatcherに渡す
3. dispatcherは実際に状態を更新してviewに反映

flux-simple-f8-diagram-with-client-action-1300w.png
画像引用元:Flux公式Doc In-Depth Overview

このように、「Storeを更新するときは、必ずDispatcherを通す」という単方向のデータフローにすることで、Storeで保持されている状態や、それに伴い発生する画面遷移の把握が容易になります。
参考:Fluxとはなんなのか

実装

ここからは、Fluxに沿った状態管理・画面更新をReact Hooksで実装していきます。

今回は、「画面に複数個のボタンがあり、それぞれ押すとon/offが切り替わる&初期状態に戻すリセットボタンも別にある」というものを作ることを想定しています。

ディレクトリ構造

Reactアプリのフォルダは、create-react-appコマンドで簡単に作成することができます。
そのsrcディレクトリ以下で、今回関係があるところのみを抜粋して表示します。

src/
   ├─ App.js
   ├─ components
   │  └─ Component.js
   ├─ contexts
   │  └─ Context.js
   ├─ actions
   │  └─ ActionCreater.js
   └─ reducers
      └─ Reducer.js

Actionの実装

Actionの実態はJSオブジェクトです。Actionは後々Dispatcherに渡されるものなので、このJSオブジェクトがDispatcher関数の引数となります。
Actionオブジェクトは、「Storeに対してどんな操作がしたいのか」というのをtypeオブジェクトに保持していることが多いです。

Actionの生成過程においては、生成actionをdispatchに渡すところまで実装することが多く、ここまでのメソッドをActionCreaterと呼びます。

以下に、ActionCreaterの例を示します。

ActionCreater.js
// buttonNoで指定された番号のボタンのon/offを切り替えるためのActionを発行→dispatcherに渡す
export function toggleButton(dispatch, buttonNo) {
    const action = {
        type: "toggle",
        data: {
            button: buttonNo
        }
    };
    dispatch(action);
}

// 全ボタンの状態を初期状態に戻すためのActionを発行→dispatcherに渡す
export function resetAllButton(dispatch) {
    const action = {
        type: "reset",
    };
    dispatch(action);
}

ここで利用している変数dispatchには、後述するdispatcher関数が格納されています。

Storeの実装

Storeの実装は、Hooksの一つであるuseReducerの第一引数のstoreで行います。
後述するdispatcherと一緒に、Hooksの一つであるuseContextを使ってApp内のどこでも使えるように共有する形になります。

まずはContextを作ります。

Context.js
import { createContext } from 'react'

export const AppContext = createContext({
    state: {
        onState: Array(10).fill(false)
    },
    dispatch: null
});

App内のどこでも呼び出せるようにしないといけないのが「StoreとDispatcher」なので、それぞれを保持するフィールド(statedispatch)を用意しています。

Contextを用意したら、今度はそれをApp全体で共有できるようにApp.jsで設定を行います。

App.js
import React, { useReducer } from 'react';
import { AppContext } from './contexts/Context';
import { AppReducer } from './reducers/Reducer';

function App() {
 const initialState = {
    isState : Array(10).fill(false)
  };

  // initialStateで、state(≒store)の初期値を設定する
  // ここで作ったdispatchには、state(≒store)を操作する関数が格納されている
  const [state, dispatch] = useReducer(AppReducer, initialState);

  return (
    <div className="App">
      // こうすることで、<AppContext>以下にある(略)部分のcomponentで
      // 変数stateとdispatchを呼び出せるようになる
      <AppContext.Provider value={{state, dispatch}}>
        ()
      </AppContext.Provider>
    </div>
  );
}

export default App;

Dispatcherの実装

dispatcherはStoreの変更・更新を行うものです。
これの実態はuseReducerの第二返り値のdispatch関数です。これの実装は第一引数AppReducerの中で行います。
つまり、言い方を変えれば「useReducerの第一引数で渡された関数が、第二返り値のdispatchに格納される」のです。

Reducerは、現在のStateと新たに生成されたActionを引数として受け取り、新しいStateを返り値として返す関数です。

(nowState, action) => newState

dispatcherの実装を担うuseReducerの第一引数「AppReducer」を、この条件に合うよう、引数を「現在のState, Action」、返り値を「新しいState」として作ります。

Reducer.js
export function AppReducer(state, action) {
    var NewonState = state.onState.slice();

    // actionのtypeによって、newStateの生成処理を変える
    switch(action.type){
        case 'toggle':
            var i = action.data.button;
            NewisPlayed[i] = !state.onState[i];
            return {onState: NewonState}
        case 'reset':
            var filledfalse = Array(10).fill(false);
            return {onState: filledfalse}
        default:
            return state;
    }
}

ViewからのAction発行

ActionCreater, dispatcher, storeが用意できたら、いよいよViewからActionを発行してStoreを変更するロジックです。
Viewでやらなければいけないのは、以下2つです。

  • ActionCreaterの呼び出し
  • ActionCreaterに引数として渡すdispatcher関数の用意

例えば、「ボタンをクリックしたら、ActionCraterのtoggleButtonを呼ぶ」ためには以下のように記述します。

Component.js
import React, { useContext } from 'react';
// ActionCreaterのインポート
import { toggleButton } from '../actions/actionCreaters';

function MyButton(props) {
    // Contextにあるdispatcher関数を取得
    const {dispatch} = useContext(AppContext);

    return (
        // 使いたい場所でActionCreater関数を呼び出す
        <button onClick={() => toggleButton(dispatch, props.buttonno)}>
            {props.buttonno}
        </button>
    );
}

ソースコードの参考文献

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

React Live Conference 2020 に参加した話

はじめに

9月11日に開催された、React Live Conference Online 2020に参加してきました!

https://user-images.githubusercontent.com/10850034/93012807-16115180-f5de-11ea-8353-daa35b6df2b9.png

今回はそのイベントに参加したレポートになります。

なかなか英語が聞き取れなかったものもあるため、内容の正確性については保証できかねますが、ご了承ください?

また、個人の興味範囲によっては記事の濃さに偏りがありますためその点もご留意お願いいたします?

(割となぐり書きになってるところは許されたい!)

トーク

The Phsycological effects of UseEffects

Speaker: Sara Vieira

序盤から最後まで、codesandboxを使って、useEffectとはどのような副作用が起きて、どういうシーンに使用できるかを丁寧に説明した内容でした。

  • useEffectは次のClass Componentのライフサイクルを扱えるよ
    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount
    • (4つめあったかな?)
  • useEffectはdependenciesに不変のstringを与えても二度と変更されないよ
  • useEffectでUnMountのtrigger

    • useEffectでUnMountのtrigger

      パターン1

      ...
      
      const [showText, setShowText] = useState(true);
      const isMounted = useRef();
      
      useEffect(() => {
          if(isMounted.current) {
              console.log(showText)
        }
      })
      
      ...
      
      return(
          <button ref={isMounted} / > 
      )
      

      パターン2

      ...
      
      const [showText, setShowText] = useState(true);
      const isMounted = useRef();
      
      useEffect(() => {
          return () => console.log('un mounted');
      })
      
      ...
      
      return(
          <button ref={isMounted} / > 
      )
      
  • 例えば、次のようなコードはエンドレスにrenderingされる

    ...
    
    const [showText, setShowText] = useState(true);
    const isMounted = useRef();
    
    useEffect(() => {
      // エンドレスrendering
        setNumber(timeClicked => timesClicked + 1)
    
        return () => {
            // renderingストップ
            setShowText((s) => !s)
        }
    }, [showText])
    
    

?このツイートに載ってるYoutubeにこのプレゼンがアップロードされてるので気になる方は見てみてください!

Building a Markdown Editor from a design perspective

Speaker: Elizabet Oliveira

  • EUIのMarkdownEditorをつくりましたという話でした。
  • EUIは、Elastic UIというUIライブラリです。

    https://user-images.githubusercontent.com/10850034/93013297-2fb49800-f5e2-11ea-9e9d-1b65408c3341.png

  • この New , Beta の表示の機能がこの作成した機能のようです。

https://user-images.githubusercontent.com/10850034/93013282-ef551a00-f5e1-11ea-9561-953c921d33fe.png

  • EuiMarkdown Editor
  • EuiMarkdown Format
  • EuiMarkdown Plugins
  • このひとはDesignerで、Figmaのデザイン話が話の中心でした。(Reactの話どこ?)

React Superpowers with GraphQL and TypeScript

Speaker: Roy Derks

https://user-images.githubusercontent.com/10850034/93013587-bff3dc80-f5e4-11ea-8a0a-446b41d069f9.png

TypeScriptとGraphQLを使ったらめっちゃいいぜ!という話です。

具体的にどうやったらCode GenerateできるかのDemoがメインでした。

  • TypeGraphQL

TypeGraphQL · Modern framework for GraphQL API in Node.js

  • GraphQL Code Generator

GraphQL Code Generator | GraphQL Code Generator

Design Systems & React in Small Team.

Speaker: Sid Kshetrapal

この回は出落ちでしたw

イントロのMovieがエンドレスに再生される、Liveならではのハプニングが?

(このイントロが5回は流れました。ギュイーンを聞くだけでニヤッとしてしまう)

https://user-images.githubusercontent.com/10850034/93013815-cd11cb00-f5e6-11ea-9fd5-bfd387fc8e3f.png

コメント欄が面白くって、「It's userEffect with no dep」やら「useEffect(() ⇒ {playVideo()})」やら「white(true) playVideo()」やら、エッジの効いたコメントが秀逸でめっちゃ笑いました。


さて、本題はDesign Systemを使うことで、パワフルで効率のよいスタイリングができるぜ!って話。

image.png

この方もFigma使ってましたね。

image.png

image.png

image.png

image.png

marginの取り方もgapとして定数化してしまうのは、Atomic Designを実装ベースで行う場合に参考になりそうですね。

image.png

image.png

chromaticを使ってDesignシステムを運用しているようです!使ってみたい。

image.png

https://www.chromatic.com/

State management & Middleware for React Applications

Speaker: Jemima Abu

class stateからuseStateに変わってコード的にどうなるかをゆっくり丁寧に触れていました。
State Managementについては実質Contextの説明とライブラリにちょっと触れただけでした。
入門者向けな発表だったのでしょうか?

  • 導入はreduxディスから
  • state管理はいろんな選択肢あるよ

image.png

  • class stateとの違い
    image.png

  • Custom Hooksについて
    image.png

  • バケツリレーについて
    image.png

  • State Managementライブラリの紹介
    image.png

image.png

  • reducer
    image.png

  • Redux Middleware
    image.png

  • createContext
    image.png

  • Provider
    image.png

NextJS on AWS

Speaker: Nader Dabit

Naderは最高のスピーカーでした!

Nextjsにおける、SSG, SSR, CSRの話です。

  • Deploying + APIS / SSR
  • Nextjsとは
    • CSR
    • SSR pre-rendering
    • SSG pre-rendering
  • pre renderingメリット
    1. Speed
    2. SEO
    3. scalability
  • static site
  • Server rendered
    • on demand
    • requires runtime execution
  • Which one?

  • SSR | SSG

    • Default - SSG
    • getStaticProps - SSG
    • getStaticPaths - SSG
    • getServerSideProps - SSR
    • API Routes - SSR
    "scriptes": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "export": "next export",
    }
    
    # export - static site generation only
    

    https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/c31c5d4d-a946-2405-fca2-f21a8b8e9f0a.png

    ページごとにSSGとSSRを選択できる!?

  • Deploy

    • SSG
    • SSR
  • AWS Amplify

    "scriptes": {
        "dev": "next dev",
        "build": "next build && next export",
        "start": "next start"
    }
    
    
  • Serverles Framework
    nextJsAWSApp がある

    https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/0429e37f-e964-be03-f104-dfbb7c3ec19b.png

  • API

    • SSR config
    • AUTH + DATA
    • SSR DATA

      https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/ae8c7bc7-9aa2-df6f-d2a0-ee24a225e28d.png

    • Auth - CSR

      https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/b558ce94-2cda-3c0d-a9e2-4de5e8abc6f2.png

    • SSR Auth

      https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/b0f165c9-27a3-57fb-ee59-627bc25210b7.png

      https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/28e8f230-cf01-6caa-97b5-617b1e43ac1e.png

The heck is CSR, SSR, & SSG in Next.js

Speaker: Ahmad Ahwais

続いてもNextjsの話です。

実際にCSR, SSR, SSGで実装されたサンプルサイトを使って話しています。

CSR、SSR、SGRの違いをhtmlのレスポンスベースで解説してくれました!

このサンプルサイトが非常に良くできており、このサイトに

  • メリット
  • デメリット
  • ユースケース
  • リスク

などなどが記載されています!

あと少しブラッシュアップすれば公開するよ!と言っていたので、期待して待っていましょう!

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/8ce5e6f1-0e50-b623-a813-57c47b99f0fd.png

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/e64e78ae-af7a-137c-9ca5-1dc8dbc174d0.png

Building React UIs visually

Speaker: Yang Zhang

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/a3afb98f-4a89-0aed-8af1-977e941b100a.png

すごい世界がすぐそこかも?

Figmaでデザインしたコードが、stateの設定やTouchableの設定も込で、codesandboxで自動デプロイされるPlasticのご紹介!

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/bed756d5-9ebd-4386-9077-de0d933e58cb.png

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/8e513e92-e68b-41d7-402f-5ebf4c0d7694.png

  • FigmaからReactコードが自動生成される
  • codesandboxへのdeployがされる

    https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/43f9605e-44fc-6862-f2e7-c9d38f2cc3e4.png

  • local のソースとsyncすることも可能

    https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/65eb2ece-8656-60a9-3edc-bbd610f4125c.png

  • 懸念

    • codegenのメンテナンスは大丈夫?
    • 一応、container componentとpresentational componentが別れてるからロジックの注入は可能だけど、codegenしたコードをて修正してしまった瞬間破綻しそう。どうやってプロテクトするんだろう
    • ツールの学習コストや運用コストが掛かる可能性。デザイナーにstate設定とかを強いるの??
  • これからのチャレンジ

    • シンプルなレイアウト

      • 自動生成によって、難解なスタイルが生成されてしまう

        https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/9c5a0c67-c3a2-2d82-0039-1e0c38cbfdc2.png

まだまだ懸念も多いようで、実験段階のようにも見えますが、今後はこういったgenerate codeのツールを使い倒していく開発フローが一般化していくのかもしれませんね。

今後にも期待です!

How to Build Your Portfolio with React

Speaker: Kapehe

ポートフォリオはだれのために、なんのためにつくるのか?

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/14395470-d163-cc94-4810-d30b99859429.png

ちゃんとしたポートフォリオサイトのメンテナンスをしたくなりました!

Kapehe

From Context API to Recoil JS

Speaker: Eric Bishard

最後はRecoilです!

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/f8478e21-031f-5612-d185-8431a0764ca0.png

既存のContextのデータフローからRecoilに置き換えるとどんなコードになるのか、Liveコーディングで置き換え作業をやってくれました。

今までなんとなくRecoilの概要を知っていたのですが、具体的なコードとして見ることで「あー、シンプルになって良さそうだな・・・」を肌感で感じました。

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/c7d4b38f-fcf4-baa2-6009-1c6af2ea4bd8.png

// AppAtom.js
import { atom, selector } from 'recoil'

export const navOpenRecoil = atom({
  key: 'navOpenRecoil',
  default: false
})
export const themeRecoil = atom({
  key: 'themeRecoil',
  default: ''
})

export const themeTextRecoil = selector({
  key: 'themeTextRecoil',
  get: ({get}) => `The theme is ${get(themeRecoil)}`
})
// useThemeInitializer.js
import { useEffect } from 'react'
import { useRecoilState } from 'recoil'
import { useMediaPredicate } from 'react-media-hook'

import { themeRecoil } from './AppAtoms'

export const useThemeInitializer = () => {
  const [ theme, setTheme] = useRecoilState(themeRecoil)
  const preferredTheme = useMediaPredicate('(prefers-color-scheme: dark)') 
   ? 'dark'
   : 'light'

  useEffect(() => {
    setTheme(preferredTheme || localStorage.getItem('recoilApp_theme'))
  }, [preferredTheme, setTheme])

  useEffect(() => {
    localStorage.setItem('recoilApp_theme', theme)
  }, [theme])

  return { theme }
}

themeTextRecoil で使っているgetは、値のフォーマットに便利そうですね。

一つ一つが小さくまとまっているのでシンプルで可読性も高いです。

atomFamilyとuseRecoilStateを使ったコードは参考になりそうですね!

...
import { atomFamily, useRecoilState } from 'recoil'

export const favoriteById = atomFamily({
  key: 'favorite',
  default: {
    name: '',
    vacancy: '',
    booked: false,
  }
})

const Favorite = ({ id }) => {
  const [{ name, vacancy, booked }, setFavorite] = useRecoilState(favoriteById(id))
...

このプレゼンテーションで使ったコードはGithubに公開されています!

httpJunkie/react-context-to-recoil-js

個人的な気づき・良かった点

  • Plasticなどの新しいサービス情報のインプット
  • Recoilの実践的コードのインプット
  • リアルタイムだからこそのイベントも楽しめる
  • コメント欄が地味に面白いし、登壇者に質問もできる
  • 人によって聞き取りやすい発音のひとと、聞き取れねぇwってひとがいた。

個人的な改善点

  • 前半と後半のレベル感の統一。前半のuseEffectとstateの話をそれぞれ30分聞いたときはマジで寝そうだった。ターゲット層が入門者としてももう少しレベル感高くていいのでは。このconfに参加している層は中級者以上だと思うんだけどな?

個人的な反省点

  • 海外confで字幕なしのリスニングするの本当にきつい。 特にスライド終了後の質疑応答を追いかけるのはきつかった・・・何もわかってなかった・・・。

所感

長丁場でしたが、結果として得るものが多くありました。いろんなライブコーディングが見れるのはいいですね!
途中、「あれ、これReactのイベントだよね・・・」と考えてしまったプレゼンもありましたが、それも含めて様々な種類の発表がありました。

海外カンファレンスって何もわからないかなーと身構えてましたが、意外となんとかなりました。一方で、英語のリスニング力がないとキャッチアップできない内容や深い理解ができないと思われますので、次回までには鍛えておこうと思いました・・・。

現場からは以上です!

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

[MERN⑦] React User Authentication

~~~~~~~~~~ (Contents) MERN ~~~~~~~~~~~
[MERN①] Express & MongoDB Setup
https://qiita.com/niyomong/private/3281af84486876f897f7
[MERN②]User API Routes & JWT Authentication
https://qiita.com/niyomong/private/c11616ff7b64925f9a2b
[MERN③] Profile API Routes
https://qiita.com/niyomong/private/8cff4e6fa0e81b92cb49
[MERN④] Post API
https://qiita.com/niyomong/private/3ce66f15375ad04b8989
[MERN⑤] Getting Started With React & The Frontend
https://qiita.com/niyomong/private/a5759e2fb89c9f222b6b
[MERN⑥] Redux Setup & Alerts
https://qiita.com/niyomong/private/074c27259924c7fd306b
[MERN⑦] React User Authentication
https://qiita.com/niyomong/private/37151784671eff3b92b6
[MERN⑧] Dashboard & Profile Management
https://qiita.com/niyomong/private/ab7e5da1b1983a226aca
[MERN⑨] Profile Display
https://qiita.com/niyomong/private/42426135e959c7844dcb
[MERN⑩] Posts & Comments
https://qiita.com/niyomong/private/19c78aea482b734c3cf5
[MERN11] デプロイ
https://qiita.com/niyomong/private/150f9000ce51548134ad
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1. Auth Reducer & Register Action

① TYPEを追加

actions/types.js
export const SET_ALERT = 'SET_ALERT';
export const REMOVE_ALERT = 'REMOVE_ALERT';
+ export const REGISTER_SUCCESS = 'REGISTER_SUCCESS';
+ export const REGISTER_FAIL = 'REGISTER_FAIL';

② Auth Reducer
~~~ 詳細説明 ~~~
(1) localStorageは、cookieのようにデータをブラウザで永続的に保存できる仕組み。デフォルトでtokenを取得する、認証成功したらlocalStorageにtokenデータを保存する。認証失敗したらtokenを削除。
(2) 認証したら、null->trueに変わる。他でも認証判定でisAuthenticatedを使用する。
(3) 認証試みる直前はLoadingする設定。認証成功または失敗したらLoading終わる->false。
(4) 認証前はname,email,avatarはnullの状態にする。
(5) Auth

reducers/auth.js
import { REGISTER_SUCCESS, REGISTER_FAIL } from '../actions/types';

const initialState = {
(1)  token: localStorage.getItem('token'),
(2)  isAuthenticated: null,
(3)  loading: true,
(4)  user: null,
};

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
    case REGISTER_SUCCESS:
(1)  localStorage.setItem('token', payload.token);
      return {
        ...state,
        ...payload,
(2)     isAuthenticated: true,
(3)     loading: false,
      };
    case REGISTER_FAIL:
(1)   localStorage.removeItem('token');
      return {
        ...state,
(1)     token: null,
(2)     isAuthenticated: false,
(3)     loading: false,
      };
    default:
      return state;
  }
}

③ reducers/index.jsにauthを追加

reducers/index.js
import { combineReducers } from 'redux';
import alert from './alert';
+ import auth from './auth';

export default combineReducers({
  alert,
+ auth,
});

④ Authアクション
~~~ 詳細説明 ~~~
(1)認証成功-> name,email,passowrdをJSON形式でAPIにPOST -> REGISTER_SUCCESSを発火。
*stringify=JSON形式に変換する
(2)payload:res.data これはtoken
(3)認証失敗->setAlertアクション->REGITER_FAILを発火
*payloadは不要(auth reducerのREGITER_FAILにpayloadはない。)
*array.forEach(element => {}); (arrayはerrors、elementは今回対象のエラー(要は認証失敗エラー))

actions/auth.js
import axios from 'axios';
import { setAlert } from './alert';
import { REGISTER_SUCCESS, REGISTER_FAIL } from './types';

// Register User
export const register = ({ name, email, password }) => async (dispatch) => {
  const config = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const body = JSON.stringify({ name, email, password });

(1)  try {
    const res = await axios.post('/api/users', body, config);

    dispatch({
      type: REGISTER_SUCCESS,
(2)   payload: res.data, //token
    });
 } catch (err) {
    const errors = err.response.data.errors;

(3) if (errors) {
      errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
    }
    dispatch({
      type: REGISTER_FAIL,
    });
  }
};

④ Registerコンポーネントにregister関数を設置。

components/auth/Register.js
...
import { setAlert } from '../../actions/alert';
+ import { register } from '../../actions/auth';
import PropTypes from 'prop-types';

+ const Register = ({ setAlert, register }) => {
...
  const onSubmit = async (e) => {
    e.preventDefault();
    if (password !== password2) {
      setAlert('Passwords do not match', 'danger');
    } else {
+     register({ name, email, password });
    }
  };
  return (
...
Register.propTypes = {
  setAlert: PropTypes.func.isRequired,
+ register: PropTypes.func.isRequired,
};

+ export default connect(null, { setAlert, register })(Register);

2. Load User & Set Auth Token

① ユーティリティフォルダ生成 -> setAuthTokenファイルを生成。

src/utils/setAuthToken.js
import axios from 'axios';

const setAuthToken = (token) => {
  if (token) {
    axios.defaults.headers.common['x-auth-token'] = token;
  } else {
    delete axios.defaults.headers.common['x-auth-token'];
  }
};
export default setAuthToken;

② TYPEを追加。

actions/type.js
...
+ export const USER_LOADED = 'USER_LOADED';
+ export const AUTH_ERROR = 'AUTH_ERROR';

③ Load Userアクションを追加

actions/auth.js
import axios from 'axios';
import { setAlert } from './alert';
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
+  USER_LOADED,
+  AUTH_ERROR,
} from './types';
+ import setAuthToken from '../utils/setAuthToken';

以下全て追加
// Load User
export const loadUser = () => async (dispatch) => {
  if (localStorage.token) {
    try {
      setAuthToken(localStorage.token);
      const res = await axios.get('/api/auth');
      dispatch({
        type: USER_LOADED,
        payload: res.data,
      });
    } catch (err) {
      dispatch({
        type: AUTH_ERROR,
      });
    }
  } else {
    dispatch({
      type: AUTH_ERROR,
    });
  }
};

// Register User
...

④ Auth ReducerにUSER_LOADEDとAUTH_ERRORを設置。

reducers/auth.js
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
+  USER_LOADED,
+  AUTH_ERROR,
} from '../actions/types';

const initialState = {
  token: localStorage.getItem('token'),
  isAuthenticated: null,
  loading: true,
  user: null,
};

export default function (state = initialState, action) {
  const { type, payload } = action;

  switch (type) {
+   case USER_LOADED:
+     return {
+       ...state,
+       isAuthenticated: true,
+       loading: false,
+       user: payload, //name,email,avatar etc.
+     };
    case REGISTER_SUCCESS:
      localStorage.setItem('token', payload.token);
      return {
        ...state,
        ...payload,
        isAuthenticated: true,
        loading: false,
      };
    case REGISTER_FAIL:
+   case AUTH_ERROR:
      localStorage.removeItem('token');
      return {
        ...state,
        token: null,
        isAuthenticated: false,
        loading: false,
      };
    default:
      return state;
  }
}

⑤ ユーザーがサイトをロードした時にいつも行う動作

ユーザーがサイトをロードした時...
(1) Tokenの有無確認。あればlocalStorageにセット。
(2) loadUserをマウント(userEffect)する。
(3) 「空の配列 ([]) を渡した場合、副作用内では props と state の値は常にその初期値のままになる。」
https://ja.reactjs.org/docs/hooks-effect.html

src/App.js
+ import React, { Fragment, useEffect } from 'react';
...
//Redux
import { Provider } from 'react-redux';
import store from './store';
+ import { loadUser } from './actions/auth';
+ import setAuthToken from './utils/setAuthToken';
import './App.css';

(2)  useEffect(() => {
(1)    setAuthToken(localStorage.token);
(2)    store.dispatch(loadUser());
(3)  }, []);

+   return (
    <Provider store={store}>
...
    </Provider>
  );
+ };
export default App;

⑥ Authアクションのregister関数が成功した場合に、loadUserを発火する。

actions/auth.js
...
// Register User
export const register = ({ name, email, password }) => 
...
  try {
    const res = await axios.post('/api/users', body, config);

    dispatch({
      type: REGISTER_SUCCESS,
      payload: res.data, //token
    });

+    dispatch(loadUser());
  } catch (err) {
...

3. User Login

① アクションにType追加。

actions/type.js
...
+ export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
+ export const LOGIN_FAIL = 'LOGIN_FAIL';

register関数をコピペして、login関数を記述。

actions/auth.js
...
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
+  LOGIN_SUCCESS,
+  LOGIN_FAIL,
} from './types';
...
//以下Register関数をコピペ
// Login User
+ export const login = ({ email, password }) => async (dispatch) => {
  const config = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

+  const body = JSON.stringify({ email, password });

  try {
    const res = await axios.post('/api/auth', body, config);

    dispatch({
+      type: LOGIN_SUCCESS,
      payload: res.data, //token
    });

    dispatch(loadUser());
  } catch (err) {
    const errors = err.response.data.errors;

    if (errors) {
      errors.forEach((error) => dispatch(setAlert(error.msg, 'danger')));
    }
    dispatch({
+      type: LOGIN_FAIL,
    });
  }
};

③ AuthReducerにLOGIN_SUCCESSと_FAILを追加。

reducers/auth.js
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
+  LOGIN_SUCCESS,
+  LOGIN_FAIL,
} from '../actions/types';

...
    case REGISTER_SUCCESS:
+    case LOGIN_SUCCESS:
      localStorage.setItem('token', payload.token);
      return {
        ...state,
        ...payload,
        isAuthenticated: true,
        loading: false,
      };
    case REGISTER_FAIL:
    case AUTH_ERROR:
+    case LOGIN_FAIL:
      localStorage.removeItem('token');
      return {
        ...state,
        token: null,
        isAuthenticated: false,
        loading: false,
      };
    default:
      return state;
  }
}

④ 認証成功時のRedirect
以下、components/auth/Register.jsも同様に追加

components/auth/Login.js
import React, { Fragment, useState } from 'react';
+ import { Link, Redirect } from 'react-router-dom';
...
+const Login = ({ login, isAuthenticated }) => {
...
+  // Redirect if logged in
+  if (isAuthenticated) {
+    return <Redirect to="/dashboard" />;
+  }
  return (
...
Login.propTypes = {
  login: PropTypes.func.isRequired,
+  isAuthenticated: PropTypes.bool,
};

+ const mapStateToProps = (state) => ({
+   isAuthenticated: state.auth.isAuthenticated,
+ });

+ export default connect(mapStateToProps, { login })(Login);

4. Logout & Navbar Links

① TYPEアクションに追加。

actions/types.js
+ export const LOGOUT = 'LOGOUT';

② アクションとリデューサーにLOGOUTを追加。

actions/auth.js
//...
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
  LOGIN_SUCCESS,
  LOGIN_FAIL,
+  LOGOUT,
} from './types';
//...
+ // Logout / Clear Profile
+ export const logout = () => (dispatch) => {
+   dispatch({ type: LOGOUT });
+ };
reducers/auth.js
import {
  REGISTER_SUCCESS,
  REGISTER_FAIL,
  USER_LOADED,
  AUTH_ERROR,
  LOGIN_SUCCESS,
  LOGIN_FAIL,
+  LOGOUT,
} from '../actions/types';
//...
    case REGISTER_FAIL:
    case AUTH_ERROR:
    case LOGIN_FAIL:
+    case LOGOUT:
      localStorage.removeItem('token');
      return {
        ...state,
        token: null,
        isAuthenticated: false,
        loading: false,
        user: null,
      };
    default:
      return state;
  }
}

③ Navbarの認証
(1) href="#!" <-シャープとエクスクラメーションマークの順番に注意。
(2) Loadingしていない場合、認証済ならauthLinks、未認証ならguestLinks
 ・{!loading && ''}の説明-> 「if not loading, then do ''」
 ・{XXX ? YYY : ZZZ}の説明-> 「if XXXX, YYY else ZZZ」
!loadingの疑問とその解決)
 initialState(reducers/auth.js)がloading: trueなので、authLinksguestLinksがNavbarに出てこないと思いきや、App.jsでloadUserを発火(USER_LOADED or AUTH_ERROR)しているので、いづれにしてもloading:falseとなる。

components/layout/Navbar.js
import React, { Fragment } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { logout } from '../../actions/auth';

 const Navbar = ({ auth: { isAuthenticated, loading }, logout }) => {
   const authLinks = (
     <ul>
       <li>
(1)      <a onClick={logout} href="#!">
           <i className="fas fa-sign-out-alt" />{' '}
          <span className="hide-sm">Logout</span>
        </a>
      </li>
    </ul>
  );

  const guestLinks = (
    <ul>
      <li>
        <a href="#!">Developers</a>
      </li>
      <li>
        <Link to="/register">Register</Link>
      </li>
      <li>
        <Link to="/login">Login</Link>
      </li>
    </ul>
  );

  return (
    <nav className="navbar bg-dark">
      <h1>
        <Link to="/">
          <i className="fas fa-code" /> Refnote
        </Link>
      </h1>
(2)   {!loading && (
        <Fragment>{isAuthenticated ? authLinks : guestLinks}</Fragment>
      )}
    </nav>
  );
};

Navbar.propTypes = {
  logout: PropTypes.func.isRequired,
  auth: PropTypes.object.isRequired,
};

const mapStateToProps = (state) => ({
  auth: state.auth,
});

export default connect(mapStateToProps, { logout })(Navbar);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】公式ドキュメントの私的まとめ

Reactがマジでわかんない
おそらく買った本がむずすぎた
公式を読んで見ることにした
その私的まとめ

JSXの導入

const element = <h1>Hello, world!</h1>;

javascriptの拡張言語
使う理由はjavascriptにHTMLを書くようにかけるので直感的にわかりやすい

式の書き方

{}の中に式を書けばいい
JSXは式だだからifにもforにも打ち込める

render

<div id="root"></div>

上記がHTMLファイルにあるとするとこれルートDOMを言うことにする

React要素をルートDOMにレンダリング(HTMLを人が見れる形にする)するには
React.render()に渡す

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
引数 意味
第1 React要素
第2 ルートDOM

Reactは意味ミュータブル
更新するには新しい要素を作成してReact.render()に渡す
以下の公式のコードには感動した
これは秒刻みで動く時計の例

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(element, document.getElementById('root'));
}

setInterval(tick, 1000);

もう一度いうと更新するには
新しく要素を作詞→React.render()に渡す、という流れ

React は必要な箇所のみを更新する

これを見たとき楽しくなった。とある本を読んで勉強するよりドキュメント読んだほうが遥かに楽しいし勉強になる。
今後は「入門」という文字の入った本に気をつけよう

話を戻すと先の時計だが、ディベロッパツールで見てみると時刻のとこだけのDOMを変更している

これはとある入門書を見て知っていたが百聞は一見にしかずとはまさにこのこと
理解ができた感動した

componentとprops

コンポーネントで部品を独立したものにして再利用可能にできる

コンポーネントはjavascript関数に似ていてpropsという入力を受け取りReactエレメントを返す
コンポーネントはReactエレメントを返す。
コンポーネントはReactエレメントを返す。
コンポーネントはReactエレメントを返す。

そうだったのか。。
コンポーネントはReactエレメントを返すのか。。

コンポーネントの定義には関数とクラスがある、これは知ってる

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

上記コードの説明も丁寧でわかりやすかった

このときコンポーネント名は大文字で始める

import React from 'react';
import ReactDOM from 'react-dom';

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

このコードを見たとき更新かなと思ったが、3行表示された、ああそうだったと、忘れてるなぁと思った

コンポーネント抽出

unction Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

上記コードがコードの分割により以下のようになる

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

function Avatar(props) {
  return (
    <img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
  );
}

function UserInfo(props) {
  return (
    <div className="UserInfo">
      <Avatar user={props.user} />
      <div className="UserInfo-name">
        {props.user.name}
      </div>
    </div>
  );
}

見やすい、スッキリ

Avatarコンポーネントの作成

このコンポーネントは自身がレンダリングされることを想定していない
propsの名前がuserがるかわれているのは
これはコンポーネント自身の観点で名前をつけることが推奨されているから

長いコードを関数化する作業に似ている

propsは読み取り専用

propsは変更してはいけない
Reactコンポーネントはpropsに対して純粋関数であること

stateとライフサイクル

先の秒刻み時計で進めていく
以下からはClockコンポーネントを再利用可能かつカプセル化されたのにする方法

function Clock(props) {
  return (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {props.date.toLocaleTimeString()}.</h2>
    </div>
  );
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElementById('root')
  );
}

setInterval(tick, 1000);

まずは見た目のカプセル化

しかし理想は以下のコードだけを書いてClock自身を更新させたいらしい

ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

これの実現にはstateを追加する必要があるという
このstateはコンポーネントによって完全プライベートなもの

関数をクラスに変換

以下の手順でクラスに変換

  1. React.Componentを継承
  2. render()というからメソッドを用意
  3. 関数の中身をそのメソッドの中に書く
  4. render()内のpropsをthis.propsへ書き換える
class Clock extends React.Component {
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.props.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

renderメソッドは更新が発生したときに毎回呼ばれるが同一DOMノード内で<Clock />をレンダーしている限り、Clockクラスのインスタンスは1つだけ使われる

つまり同一DOM無いであればいくらrenderメソッド内のClockを呼び出しても同じインスタンスが使われる、保持したものはそのまま使えるということ、かな

クラスにローカルなstateを追加

以下の3ステップでdateをpropsからstateに移す

  1. render()メソッド内のthis.props.datethis.state.dateに書き換える
  2. this.stateの初期状態を設定するクラスコンストラクタを設定
  3. <Clock />要素からdateプロパティを削除

2では親のコンストラクタへのpropsの渡し方に注意
クラスコンポーネントでは常にpropsを引数として親クラスのコンストラクタを呼び出す必要がある

super(props)

とある入門書ではなぜこれを書くのか。
初見でこれを見たときなんで親クラスのを呼び出しているのかわからなかった。
今もなぜだかわからないけどそうおいうものだとわかっただけもスッキリする。

クラスにライフサイクルメソッドを追加

コンポーネント破棄時にリソースを開放したほうがいい

タイマー設定は最初にClockが描画(マウント)されるとき。
タイマークリアはClockが生成したDOMが削除(アンマウント)されるとき

このようなライフサイクルメソ度を使っていく

componentDidMount

コンポーネントがマウントされた直後に呼ばれる

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

タイマーIDをthis上に格納しているのには訳がある

componentWillUnmount

コンポーネントがアンマウントされた直後によばれる
このタイミングでクリアするのがいい

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

最後にtick()を作る
コンポーネントがローカルstateの更新をするためにthis.setStateを使う

  tick() {
    this.setState({
      date: new Date()
    });
  }

何が起きたかの振り返り

  1. <Clock />がReactDOM.render()に渡されると.ReactはClockコンポ-ネントのコンストラクタを呼び出す。このときにstateを初期化。
  2. 次にReactはrender()を呼び出す。
  3. Clockの出力されるとReactはcomponentDidMountを呼び出す。
  4. tick()が実行されるとsetStateでstateが変更される。それをReactが感知してrender()を再度呼び出す。そしてthis.state.dataが前回と違っていいるためReactはDOMを更新
  5. Clockコンポーネントが削除されたらcomponentWillUnmountでタイマーが止まる

stateを正しく使用する

stateを直接変更しない

以下は再レンダーされない

this.state.comment = 'Hello';

setStateを使えばいい

this.setState({comment: 'Hello'});

直接書き換えた場合、再レンダリングされない
Reactが変更を検知できないため。

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

【React Native】FlatListのレンダリングについて実験&改善してみた

React(React Native)でstateの更新時にどのように再レンダリングが走るか、理解が甘かったので実験してみました。

今回はFlatListを例にしています。

リストはFlatListを使っておけば取り敢えずOKや〜♪
と思っていましたが、思った以上に不要なレンダリングが走っていることが分かりました。

リストは項目数が増えると再レンダリングの影響も大きいので、改善の効果も大きいと思います。

実験1 FlatListと関係のないstateが更新されたときに、FlatListも更新される件。

カウンターとFlatListを持つ画面を用意して、カウンターを更新したときにどのようなrenderが走るか確認する。

iPhone_11__13_5_.png

// 上記の画面のコード

export default function App() {
  // FlatListと関係のないcount
  const [count, setCount] = useState<number>(1);
  // FlatListの子要素
  const [children, setChildren] = useState<Child[]>([]);

  useEffect(() => {
    setChildren([{ id: "1" }, { id: "2" }, { id: "3" }]);
  }, []);

  const renderItem = ({ item }: { item: Child }) => <ChildItem id={item.id} />;

  const keyExtractor = (item: Child) => item.id;

  return (
    <SafeAreaView style={styles.container}>
      {/* ヘッダー */}
      <View style={styles.header}>
        <Text style={styles.text}>Parent</Text>
        <View style={styles.row}>
          <Text style={styles.count}>{`count: ${count}`}</Text>
          <Button
            onPress={() => setCount(count + 1)}
            title="Update Parent State"
          />
        </View>
      </View>
      {/* リスト */}
      <FlatList
        data={children}
        renderItem={renderItem}
        keyExtractor={keyExtractor}
      />
    </SafeAreaView>
  );
}

why-did-you-renderというライブラリを利用してみます。
https://github.com/welldone-software/why-did-you-render

どのpropsのせいで再レンダリングが走ったかを教えてくれます。
PureComponentやReact.memoを使うときに助けになります。
てか名前が面白いです。

実験1 (改修前)

上記の初期状態でcountを更新してみます。
FlatListのレンダリングも走っています。

さらにFlatListの子コンポーネントChildItemたちもレンダリングされています。
childrenの項目数が増えると影響が大きそうです。

75f33a77772f80db8f9bac15130d8e9b.gif

why-did-you-render のログを見てみます。
props.renderItemが同じ名前だけど別のオブジェクトだよ」
とのこと。
props.keyExtractorも同様です。

performance-sample_on_Expo_Developer_Tools.png

renderItem, keyExtractorの関数が再生成されるため、実際は内容の変更がないFlatListの再レンダリングが走ってしまいます。
この辺はJavaScriptのややこしいところ。

実験1 (改修後)

renderItem, keyExtractoruseCallbackを使って再生成しないようにしてみます。

  const renderItem = useCallback(
    ({ item }: { item: Child }) => <ChildItem id={item.id} />,
    []
  );

  const keyExtractor = useCallback((item: Child) => item.id, []);

FlatListの再レンダリングが無くなった!

2d0a6e3ac36fe1d5f7e5ff45597fbdc3.gif

(補足)
FlatList自体はPureComponentなので、FlatListにわたすpropsに変化がなければ、その配下は再レンダリングされない。しかし今回のように関数が再生成されて別オブジェクトになるのは気を付けないとですね。
https://reactnative.dev/docs/flatlist#example

実験2 リストに項目追加したときに、既存の項目まで再レンダリングされる件

childrenに項目を追加する「Add Child」ボタンを追加します。
FlatListに項目が追加されたときに、どのようにレンダリングが走るか確認してみます。

6b360ba5cf39411d257f4a4281077b34.gif

// App.tsx
// Add Childボタンの追加
<Button
  onPress={() => {
    console.log("On Press Add Child");
    setChildren([
      ...children,
      { id: (children.length + 1).toString() },
    ]);
  }}
  title="Add Child"
/>

実験2 (改修前)

こんな感じで子コンポーネントにconsole.logを仕込んで、リスト項目追加したときの挙動を確認してみます。

export const ChildItem = ({ id }: Props) => {
  console.log("render ", id);
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Flat List Item</Text>
      <Text style={styles.text}>{id}</Text>
    </View>
  );
};

追加した4だけでなく、既存の1~3もrenderされています。
(2周分renderされているのは謎)
iPhone_11__13_5__と__26__performance-sample_on_Expo_Developer_Tools.png

実験2 (改修後)

子コンポーネントをmemo化してみます。
memo化することでChildItemのpropsに変更がない場合(浅い比較)は再レンダリングしなくなるはず。

export const ChildItem = React.memo(({ id }: Props) => {
  console.log("render ", id);
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Child</Text>
      <Text style={styles.text}>{id}</Text>
    </View>
  );
});

新しく追加した4だけレンダリングされています!

iPhone_11__17F61__と__130__performance-sample_on_Expo_Developer_Tools.png

実験3 親のstateが更新されると問答無用で子や孫も再レンダリングされる件

FlatListとは直接関係ないですが、気になったので実験してみました。

こんな感じで、Large > Middle > Smallの入れ子になったコンポーネントを用意しました。
そしてこれらと関係のないカウンターのstateを更新した再の再レンダリングを調べてみます。

iPhone_11__17F61_.png

export const LargeItem = () => {
  console.log("render LargeItem");
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Large Item</Text>
      <MiddleItem />
    </View>
  );
};

export const MiddleItem = () => {
  console.log("render MiddleItem");
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Middle Item</Text>
      <SmallItem />
    </View>
  );
};

export const SmallItem = () => {
  console.log("render SmallItem");
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Small Item</Text>
    </View>
  );
};

実験3(改修前)

LargeItemさらにMiddleItem, SmallItemまで再レンダリングされています。

eeb80bea4ba289eb2a09d9c1c028fe18.gif

実験3(改修後)

例によってmemo化します。
以下LargeItemの例。MiddleItem, SmallItemも同様にReact.memoで囲みます。

export const LargeItem = React.memo(() => {
  console.log("render LargeItem");
  return (
    <View style={styles.container}>
      <Text style={styles.text}>Large Item</Text>
      <MiddleItem />
    </View>
  );
});

不要なレンダリングが無くなった!

335d45e335bf02d8a4a2ce136b261a7a.gif

まとめ

調べてみると、意外なところでレンダリングが走っていることが分かりました。

アロー関数が再生成されることや、propsが変わってない子コンポーネントまでレンダリングされることはちょっと罠ですね(?)。

FlatListなどは項目数が多くなると、その分レンダリング負荷への影響も大きいので、パフォーマンスが気になったときは見直してみる価値はあると思います。

一方で、初めからこのようなパフォーマンス改善はしないことが推奨されています。

useCallbackmemoは、気を付けてdependency listを設定していないと、思わぬバグを生みかねません。(そしてこの手のバグは発見しにくい..)

またシンプルな画面なら、memo化によるprops比較が逆にコストになる場合もあります。

なのでパフォーマンスが気になったときに、これらを疑ってみると良いと思います。

参考

お前らのReactは遅い
https://qiita.com/teradonburi/items/5b8f79d26e1b319ac44f

雰囲気で使わない React hooks の useCallback/useMemo
https://qiita.com/seya/items/8291f53576097fc1c52a

React.memoを利用したパフォーマンスチューニング
https://note.com/green_grass_grow/n/n0703595eafb9

React Native Performance Optimisation With Hooks
https://dev.to/ltsharma24/performance-optimisation-react-native-with-hooks-a77

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

Next.js × TypeScript × Firebase AuthenticationでGoogle認証を実装する

概要

Next.js × TypeScript × Firebase Authenticationを用いたgoogle認証機能を実装してみました。

実装(Firebase側)

プロジェクトを作成する

Firebaseにアクセスし、新しいプロジェクトを作成します。
作成が完了したら、『ウェブ』というアイコンをクリックし、アプリ名の登録を行います。

スクリーンショット 2020-09-13 15.00.41.png

スクリーンショット 2020-09-13 15.01.35.png

アプリ名の登録が完了すると、下記のようなスクリプトが表示されるかと思います。
こちらはnextアプリ側で使うので、コピーしておきます。

var firebaseConfig = {
    apiKey: xxxxx
    authDomain: xxxxx,
    databaseURL: xxxxx,
    projectId: xxxxx,
    storageBucket: xxxxx,
    messagingSenderId: xxxxx,
    appId: xxxxx,
    measurementId: xxxxx
};

Google認証を有効にする

Firebaseのコンソール画面のSign-in methodタブにあるログインプロバイダの中でGoogle認証を有効化・無効化する部分があるので、まずそちらで有効化します。

スクリーンショット 2020-09-13 14.56.05.png

これでFirebase側での設定は終了です。

実装(Next.js側)

インストール

firebase Authentication機能を利用したいので、
アプリケーションディレクトリにてfirebaseをインストールします。

myapp
$ yarn add firebase

必要ファイルを生成

.env

アプリケーションディレクトリにて.envを生成します。

myapp
$ touch .env
$ vim .env

.envにコピーしたスクリプトの値を記述していきます。
各個人、値が違うので適宜記述してください。

myapp/.env
FIREBASE_API_KEY=xxxxx
FIREBASE_AUTH_DOMAIN=xxxxx
FIREBASE_DATABASE_URL=xxxxx
FIREBASE_PROJECT_ID=xxxxx
FIREBASE_STORAGE_BUCKET=xxxxx
FIREBASE_MESSEGING_SENDER_ID=xxxxx
FIREBASE_APP_ID=xxxxx
FIREBASE_MEASUREMENT_ID=xxxxx

設定し終えたら、.gitignoreに.envがpushされないように追加を忘れないでください。

next.config.js

.envの値をnextアプリでも利用できるようにnext.config.jsに記述していきます。

myapp/next.config.js
module.exports = {
  env: {
    FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
    FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
    FIREBASE_DATABASE_URL: process.env.FIREBASE_DATABASE_URL,
    FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
    FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
    FIREBASE_MESSEGING_SENDER_ID: process.env.FIREBASE_MESSEGING_SENDER_ID,
    FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
    FIREBASE_MEASUREMENT_ID: process.env.FIREBASE_MEASUREMENT_ID,
  },
}

Firebase.ts

.envの値を利用し、Firebase.tsに設定を記述していきます。

myapp/src/utils/Firebase.ts
import firebase from 'firebase'

const config = {
  apiKey: process.env.FIREBASE_API_KEY,
  authDomain: process.env.FIREBASE_AUTH_DOMAIN,
  databeseURL: process.env.FIREBASE_DATABASE_URL,
  projectId: process.env.FIREBASE_PROJECT_ID,
  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.FIREBASE_MESSEGING_SENDER_ID,
  appId: process.env.FIREBASE_APP_ID,
  measurementId: process.env.FIREBASE_MEASUREMENT_ID,
}
// firebase.appsをチェックし、ロードされているかどうかを確認。
// なければinitializeAppを実行
if (!firebase.apps.length) {
  firebase.initializeApp(config)
}

export default firebase

Auth.tsxでContextの作成

ユーザー情報の値をグローバルに持ち、バケツリレーを回避しつつ、Auth.tsx配下のコンポーネントたちに値が渡るようにContextの作成を行います。

srx/context/Auth.tsx
import { User } from 'firebase';
import { FC, createContext, useEffect, useState } from 'react';

import firebase from '../utils/firebase';

type AuthContextProps = {
  currentUser: User | null | undefined
}

const AuthContext = createContext<AuthContextProps>({ currentUser: undefined });

const AuthProvider: FC = ({ children }) => {
  const [currentUser, setCurrentUser] = useState<User | null | undefined>(
    undefined
  );

  useEffect(() => {
    // ログイン状態が変化するとfirebaseのauthメソッドを呼び出す
    firebase.auth().onAuthStateChanged((user) => {
      setCurrentUser(user);
    })
  }, []);

  /* 下階層のコンポーネントをラップする */
  return (
    <AuthContext.Provider value={{ currentUser: currentUser }}>
      {children}
    </AuthContext.Provider>
  )
}

export { AuthContext, AuthProvider }

_app.tsxにてComponentをAuthProviderでラップすることを忘れないでください。

src/pages/_app.tsx
import React, { useEffect } from 'react'
import { AppProps } from 'next/app'
import { AuthProvider } from '../context/Auth'

const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
  return (
    ------
     </AuthProvider>
       <Component {...pageProps} />
     </AuthProvider>
    ------
  )
}

export default MyApp

サインイン画面

それでは実際にサインアップ画面の実装を行っていきます。
尚、今回はUI側の実装は簡易的なものになりますので、UI実装は適宜実装してください。

src/signIn.tsx
import { FC, useEffect, usecontext } from 'react';
import Router from 'next/router';
import firebase from '../../utils/firebase';

const SignIn: FC = () => {
  const { currentUser } = useContext(AuthContext);

  useEffect(() => {
    currentUser && Router.push('/')
  }, [currentUser]);

  const login = () => {
    const provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithRedirect(provider);
  }
  return (
     <div className="container">
      <button onClick={login}>googleでログインする</button>
     </div>
  )
}
  • useContextによって親よりすべての子たちがcurrentUserという現在ログインしているユーザー情報の値を利用できるようにしています。
    useStateと合わせて利用することでpropsによりバケツリレーを行うことなく、情報更新や値渡しが可能になり、複雑にならずに済むことがメリットです。

  • useEffectによってもし現在クライアントがログインしていた場合、ホーム画面へリダイレクトするようにしています。[currentUser]の部分はcurrentUserの値が変更された場合、useEffectが発火するようにしています。

動作確認

実装が完了したら、ホーム画面でcurrentUserのメールアドレスなどを表示させてみましょう。

参考リンク

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

フロントエンド(React関係)備忘録

Reactに関連したものの脳内整理用

React

  • 宣言的記述
    • 対義: 手続き的記述
      • 例: jQueryのイベントベースの処理
    • 読みやすい(何がしたいものか明瞭になる)
  • コンポーネント
    • ページの構成要素をコンポーネントというパーツに分けられる
    • 状態管理が容易
    • 再利用しやすい
  • SPA想定
    • Reactはページの差分更新をコンポーネントを使って行いやすい

どんな感じか

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

ReactDOM.render(<App />, document.getElementById('root'));
app.jsx
import React, { Component } from 'react';
import './App.css';
import Form from './Form';

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      // ...
    }

    render() {
      return (
        <div>
          <Form />
          // ...
        </div>
      )
    }
  }
}
export default App;

こんな感じでComponentを指定の場所にrenderする。
JSXを使うのでwebpackなどのbuildを通す

Redux

  • FluxというアーキテクチャパターンをReact用に提供したもの
    • [Action] → [Dispatcher] → [Store] → [View]
    • 利用側とストアとの関係は参照だけ
    • 更新したい場合はDispatcherを通すというルールによって更新処理を集約する
    • 更新処理が散らばっていないのでデータの状態管理がしやすい
  • Reduxは
    • ただひとつのStoreをページ内で利用する
    • Dispatcherの代わりにStoreのfunctionに更新処理を集約

どんな感じか

index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore} from 'redux';
import Reducer from './reducers/index.js'
import { Provider } from 'react-redux';
import App from './App';

const store = createStore(Reducer)

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
store.js
import { combineReducers, createStore, compose, applyMiddleware} from 'redux'
import { Reducer, State } from './reducer'
import thunk from "redux-thunk"

const initData = {
  // ...
}

export function AppReducer(state = initData, action) {
  switch (action.type) {
    case '...';
      // return ...;
    case '...';
      // return ...; 
  }
}
const store = createStore(
    combineReducers<AppState>({
      state: Reducer
    }),
    storeEnhancers(applyMiddleware(thunk))
)

export default createStore(AppReducer);
App.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import './App.css';
import Form from './Form';

const mapStateToProps = (state) => {
  return {
    // ...
  }
}

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      // ...
    }

    render() {
      return (
        <div>
          <Form />
          // ...
        </div>
      )
    }
  }
}

export default App = connect(mapStateToProps)(App); 
Form.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {  }

const mapStateToProps = (state) => {
  return {
    // ...
  }
}

class Form extends Component {
  constructor(props) {
    super(props)
    this.state = {
      // ...
    }

    doSubmit(e) {
      let action = ...
      this.props.dispatch(action);
    }

    render() {
      return (
        <div>
          // ...
        </div>
      )
    }
  }
}

export default App = connect(mapStateToProps)(App); 

Next.js

ブラウザが初期ページを読み込むタイミングで、サーバ側のNode.jsでReactアプリが実行され、初期HTMLを生成(SSR)します。ブラウザがそれを読み込んで初期表示し、引き続きReactアプリを実行し、仮想DOMの更新や、SPAとしての実行にうまいこと繋げてくれます

  • 静的サイト生成(SSG)もできる
    • 事前に静的なHTMLやjsなどを作って、それをホストする形式

https://qiita.com/uehaj/items/1b7f0a86596353587466#gatsby%E3%81%AE%E5%8B%95%E4%BD%9C

Reactアプリをビルド時に1回実行し、HTMLを生成します。HTMLを生成する動作はSSRと同様なのですが、サーバ上ではなく、ビルドマシン上で実行することが異なります。このHTMLをJSと共にデプロイし、ブラウザはそれを初期ページとして読み込み、SSRと同様にReactアプリの実行が再度なされ、仮想DOM更新、SPAとしての実行、Reduxステートなどが引き継がれます

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

Next.js + styled-components + TypeScript でNetlifyにデプロイ失敗した話

Next.js + styled-components + TypeScript の構成でNetlifyにデプロイしたら失敗が続いていてハマったのですが、単純な修正であっさり解決しました。備忘録として残しておきたいと思います。

ちなみに私はTypeScriptを始めて1週間程度です。
経験者の方なら明快に分かる記事かもしれません。

インストールしたパッケージ

package.json
{
  "name": "portfolio",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "export": "next export",
    "deploy": "npm run build && npm run export"
  },
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.27",
    "@fortawesome/free-solid-svg-icons": "^5.12.1",
    "@fortawesome/react-fontawesome": "^0.1.9",
    "@types/styled-components": "^5.1.3",
    "moment": "^2.27.0",
    "next": "^9.5.3",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "styled-components": "^5.0.1"
  },
  "devDependencies": {
    "@types/node": "^14.10.0",
    "@types/react": "^16.9.49",
    "babel-plugin-styled-components": "^1.10.7",
    "babel-plugin-transform-define": "^1.3.0",
    "typescript": "^4.0.2"
  }
}

コンポーネント構成

※最低限に簡略化しています
※Next.jsとstyled-compomentsのAPIについての説明は省略します

components/Layout.tsx
・Styled-Components付きのコンポーネントを読み込む
・下記のGlobalStyleからもインポートしている

util/GlobalStyle.js
・リセットCSSやタイプセレクタ用等のグローバルCSSを、const GlobalStyle = createGlobalStyle`...`として記述

pages/index.tsx
・components配下のコンポーネントを配置
Layout.tsx
import Head from 'next/head';
import GlobalStyle from '../utils/GlobalStyle';
import Header from './Header';
import Footer from './Footer';

type LayoutProps = {
  title: string;
  children: React.ReactNode;
};

export default function Layout({ title, children }: LayoutProps) {
  return (
    <>
      <GlobalStyle />
      <Head>
        <title>{title}</title>
        <meta charSet="utf-8" />
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      </Head>
      <Header />
      {children}
      <Footer />
    </>
  );
}

GlobalStyle.js
import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
   リセットCSS...
   タイプセレクタへのCSS...
`
export default GlobalStyle;

index.tsx
import Layout from '../components/Layout';

export default function Home() {
  return (
      <Layout title="Top page">
         私のWebサイトへようこそ!
      </Layout>
  );
}

問題

Netlifyで何度デプロイしても失敗します。
ビルドコマンドと出力先フォルダの指定は問題ないようでした。

ここでログを見るとこのようになっています。

4:32:27 AM: info  - Creating an optimized production build...
4:32:33 AM: Failed to compile.
4:32:33 AM: 
4:32:33 AM: ./components/Layout.tsx:2:25
4:32:33 AM: Type error: Cannot find module '../utils/GlobalStyle' or its corresponding type declarations.
4:32:33 AM:   1 | import Head from 'next/head';
4:32:33 AM: > 2 | import GlobalStyle from '../utils/GlobalStyle';

不思議なのが、手元のマシンでnpm run deployするとエラーが出ないことです。
また、「Cannot find module or its corresponding type declarations」等で調べても分からずじまいのままでした。

型指定が必要なのかな?と思い、styled-componentsの公式サイトを見たらd.tsファイルの説明があったので作成しようと思いましたが、これはテーマの型指定でありcreateGlobalStyleには関係が無さそうです。

解決

ここでグローバルスタイル用ファイルの拡張子を.jsから.tsに変えてみることにしました。

util/GlobalStyle.js → util/GlobalStyle.ts

するとあっさりNetlifyへのビルドが通りました。
とりあえずTypeScript対応すれば拡張子は.tsにしておくと良いのかな?ということを感じました。

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

laravel + reactで予約機能付きSPAのHPを作成する

おおまかな流れ

1、laravelでreactが使えるようにターミナルから必要なものをインストールする。
2、ルーティングを設定する
3、予約機能の作成

laravelでreactを使えるようにする

ターミナルで以下のコマンド群を実行する

まずは、laravelのuiパッケージをインストールする
composer require laravel/ui

次に、reactを使えるようにする
php artisan ui react

最後にnpmをインストールし、自動的にファイルの変更を検知してコンパイルをしてもらう
npm install && npm run watch-poll

reactで開発できるようにするため、laravelのファイル群を変更する

一番最初にリクエストを投げた時にレスポンスで返ってくるbladeファイルを作成する。
下記のコードにある<div id="index"></div>の部分に注目する。
ここ部分にreactで作成する箇所が埋め込まれるイメージ。

top.blade.php
<body>
    <div id="index"></div>
</body>

次は、下記のコードにある
if (document.getElementById('index')) {
ReactDOM.render(<Index />, document.getElementById('index'));
}

の部分に注目する。
先ほど設定したid・indexの部分にこのIndexコンポーネントを埋め込む

Index.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Top from './Top';
import Greetings from './Greetings';
import Profile from './Profile';
import Fee from './Fee';
import Contact from './Contact';
import Access from './Access';
import Reservation from './Reservation';
import '../../../public/css/index.css';

export default class Index extends Component {
    render() {
        return (
            <div>
                    <BrowserRouter>
                        <Switch>
                            <Route exact path="/" component={Top} />
                            <Route exact path="/greetings" component={Greetings} />
                            <Route exact path="/profile" component={Profile} />
                            <Route exact path="/reservation" component={Reservation} />
                            <Route exact path="/fee" component={Fee} />
                            <Route exact path="/contact" component={Contact} />
                            <Route exact path="/access" component={Access} />
                        </Switch>
                    </BrowserRouter>
            </div>
        );
    }
}

if (document.getElementById('index')) {
    ReactDOM.render(<Index />, document.getElementById('index'));
}

SPAを作成するためにreact-router-domをインストールして設定する

ターミナルでnpm install react-router-domを実行すると、package.jsonにインストールしたモジュールが記述される。
これでモジュールを使用する準備が整った。

まずは、BrowserRouter、Route、Switchモジュールをimportする。
次に、BrowserRouter、Route、Switchの順に入れ子にする。

<Route path="/" component={Top} />の解説をする。
/にアクセスがあった場合に、Topコンポーネントを表示させるということである。

ここに遷移させるためのアンカー部分は、Linkモジュールを使用する。下記に例を示す。
Linkモジュールはレンダーされるとaタグになる。このLinkモジュールで生成したアンカーをクリックすると、
上記で設定した<Route path="/" component={Top} />の通り、Topコンポーネントが表示される。

ルーティングはこれで完成。

Header.js
<Link to="/"</Link>
Index.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Top from './Top';

export default class Index extends Component {
    render() {
        return (
            <div>
                    <BrowserRouter>
                        <Switch>
                            <Route path="/" component={Top} />
                        </Switch>
                    </BrowserRouter>
            </div>
        );
    }
}

予約機能の作成

コードが長くてみにくいので必要な部分をピックアップして解説していく。

下記のコードにあるconstructor内では、stateを定義したり関数をバインドする。
これをしないとstate・関数が使えない。

constructor(props) {
super(props)
this.state = {
date: "",
}
this.onDateChange = this.onDateChange.bind(this);
}

下記のコードは、stateであるdateにフォームで入力された値を格納している。
onDateChange(e) {
this.setState({ date: e.target.value })
}

下記のコードのdisabled={this.state.isDisabled}はisDisabledのstateがtrueの場合はsubmitできないようにしている。
onClick={this.postReservation}は、submitした際にpostReervationメソッドが実行される。
postReservationメソッドについての説明はこの後に行う。
<Button className="float-right" id="btn" variant="contained" disabled={this.state.isDisabled} onClick={this.postReservation} color="primary">
送信する
</Button>

下記のコードの解説をする。
定数dataにフィールドに入力された値を格納する。
axiosを使用して非同期通信を行なっている。post送信で/reservationに対して先ほど定義した定数dataを送っている。
では。送信した先の/reservationを見ていく。

    const data = {
            date: this.state.date,
        }

            axios.post('/reservation', data)

先ほど解説した通りに非同期通信を行うと下記の処理が実行される。
先ほど定数dataで送った値が$requestに格納されている。
ReservationSendmailクラスをインスタンス化する際に$date渡す。

ReservationController.php
<?php

namespace App\Http\Controllers;

use App\Mail\ReservationSendmail;
use Illuminate\Support\Facades\Mail;
use App\Http\Requests\ReservationRequest;

class ReservationController extends Controller
{
    public function store(ReservationRequest $request)
    {
        $date = $request->date;
        $to = 'test@gmail.com';
        Mail::to($to)->send(new ReservationSendmail($date);

        return;
    }
}

次はReservationSendmailクラスをみる。

ReservationSendmail.php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ReservationSendmail extends Mailable
{
    use Queueable, SerializesModels;

    private $date;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($date)
    {
        $this->date = $date; //ReservationControllerから送られてきた$dateをプロパティである$this->dateに格納している。
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this
            ->from('tatataabcd@gmail.com') //送信元のメールアドレス
            ->subject('自動送信メール') //メールのタイトル
            ->view('reservation.mail') //viewはreservation.mail.blade.phpを使用
            ->with([
                'date' => $this->date,  //先ほど格納したプロパティをreservation.mail.blade.phpでも使えるようにしている
            ]);
    }
}

次にreservation.mail.blade.phpを見る

mail.blade.php
お問い合わせ内容を受け付けました。<br>
<br>
■日時<br>
{!! $date !!}<br> この内容がReservationControllerのtoに指定したメールアドレスに送信される。
Reservation.js
import React, { Component } from 'react';
import moment from 'moment';
import Header from './Header';
import { Button } from '@material-ui/core';
import '../../../public/css/reservation.css';

class Reservation extends Component {
    constructor(props) {
        super(props)
        this.state = {
            date: "",
            name: "",
            phone: "",
            email: "",
            age: "",
            state: "",
            gender: "",
            isDisabled: false,
            errors: {
                date: [],
                name: [],
                phone: [],
                email: [],
                age: [],
                state: [],
                gender: [],
            }
        }
        this.onDateChange = this.onDateChange.bind(this);
        this.postReservation = this.postReservation.bind(this);
    }

    onDateChange(e) {
        this.setState({ date: e.target.value })
    }

    postReservation(e) {
        if (this.state.date !== "" && this.state.name !== "" && this.state.phone !== "" && this.state.email) {
            this.setState({
                isDisabled: true
            });
        }

        axios
            .post('/reservation', data)
            .then(response => {
                alert('予約を受け付けました。')
                this.setState({
                    isDisabled: false
                });

                this.setState({
                    errors: []
                });
            })
            .catch(error => {
                this.setState({
                    isDisabled: false
                });
                console.log(error.response.data.errors);
                const errors = this.state.errors;

                // 次の日から
                const nextDay = moment().add('1', 'd').format('YYYY-MM-DD');
                let reservationDay = this.state.date;
                reservationDay = reservationDay.slice(-16, -6);

                // 水木のみ
                const date = moment(this.state.date);
                const dayOfWeek = date.day();

                // 時間指定
                const hour = date.hour();
                const minute = date.minute();
                const hourAndMinute = hour + ':' + minute;

                if (this.state.date === "" ||
                    nextDay > reservationDay ||
                    dayOfWeek !== 3 &&
                    dayOfWeek !== 4 ||
                    hourAndMinute !== 10 + ':' + 0 &&
                    hourAndMinute !== 11 + ':' + 0 &&
                    hourAndMinute !== 13 + ':' + 0 &&
                    hourAndMinute !== 14 + ':' + 0 &&
                    hourAndMinute !== 15 + ':' + 0 &&
                    hourAndMinute !== 15 + ':' + 30 &&
                    hourAndMinute !== 16 + ':' + 0 &&
                    hourAndMinute !== 16 + ':' + 30
                ) {
                    errors.date = error.response.data.errors.date[0];
                } else {
                    errors.date = "";
                }
                this.setState({
                    errors: errors
                });

                if (this.state.name === "") {
                    errors.name = error.response.data.errors.name[0];
                } else {
                    errors.name = "";
                }
                this.setState({
                    errors: errors
                });

                if (this.state.phone === "" || isNaN(this.state.phone)) {
                    errors.phone = error.response.data.errors.phone[0];
                } else {
                    errors.phone = "";
                }
                this.setState({
                    errors: errors
                });

                if (this.state.email == "" || this.validateEmail(this.state.email)) {
                    errors.email = error.response.data.errors.email[0];
                } else {
                    errors.email = "";
                }
                this.setState({
                    errors: errors
                });
            });
    }

    render() {
        return (
            <React.Fragment>
                <Header />
                <div className="container">
                    <div className="row">
                        <div className="col-12">
                            <h1 class="h3 mb-5 mt-5 text-center">予約フォーム</h1>
                            <div className="form-group">
                                <span>予約日時</span>
                                <input className="form-control" type="datetime-local" name="date"
                                    value={this.state.date} onChange={this.onDateChange} />
                                <p className="err-msg">{this.state.errors.date}</p>
                            </div>


                            <Button className="float-right" id="btn" variant="contained" disabled={this.state.isDisabled} onClick={this.postReservation} color="primary">
                                送信する
                            </Button>
                        </div>
                    </div>
                </div>
            </React.Fragment >
        );
    }
}

export default Reservation;

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

react + laravelで予約機能付きHPを作成する

おおまかな流れ

laravelでreactを使えるようにする

ターミナルで以下のコマンド群を実行する

まずは、laravelのuiパッケージをインストールする
composer require laravel/ui

次に、reactを使えるようにする
php artisan ui react

最後にnpmをインストールし、自動的にファイルの変更を検知してコンパイルをしてもらう
npm install && npm run watch-poll

reactで開発できるようにするため、laravelのファイル群を変更する

一番最初にリクエストを投げた時にレスポンスで返ってくるbladeファイルを作成する。
下記のコードにある<div id="index"></div>の部分に注目する。
ここ部分にreactで作成する箇所が埋め込まれるイメージ。

top.blade.php
<body>
    <div id="index"></div>
</body>

次は、下記のコードにある
if (document.getElementById('index')) {
ReactDOM.render(<Index />, document.getElementById('index'));
}

の部分に注目する。
先ほど設定したid・indexの部分にこのIndexコンポーネントを埋め込む

Index.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Top from './Top';
import Greetings from './Greetings';
import Profile from './Profile';
import Fee from './Fee';
import Contact from './Contact';
import Access from './Access';
import Reservation from './Reservation';
import '../../../public/css/index.css';

export default class Index extends Component {
    render() {
        return (
            <div>
                    <BrowserRouter>
                        <Switch>
                            <Route exact path="/" component={Top} />
                            <Route exact path="/greetings" component={Greetings} />
                            <Route exact path="/profile" component={Profile} />
                            <Route exact path="/reservation" component={Reservation} />
                            <Route exact path="/fee" component={Fee} />
                            <Route exact path="/contact" component={Contact} />
                            <Route exact path="/access" component={Access} />
                        </Switch>
                    </BrowserRouter>
            </div>
        );
    }
}

if (document.getElementById('index')) {
    ReactDOM.render(<Index />, document.getElementById('index'));
}

SPAを作成するためにreact-router-domをインストールして設定する

ターミナルでnpm install react-router-domを実行すると、package.jsonにインストールしたモジュールが記述される。
これでモジュールを使用する準備が整った。

まずは、BrowserRouter、Route、Switchモジュールをimportする。
次に、BrowserRouter、Route、Switchの順に入れ子にする。

<Route path="/" component={Top} />の解説をする。
/にアクセスがあった場合に、Topコンポーネントを表示させるということである。

ここに遷移させるためのアンカー部分は、Linkモジュールを使用する。下記に例を示す。
Linkモジュールはレンダーされるとaタグになる。このLinkモジュールで生成したアンカーをクリックすると、
上記で設定した<Route path="/" component={Top} />の通り、Topコンポーネントが表示される。

ルーティングはこれで完成。

Header.js
<Link to="/"</Link>
Index.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Top from './Top';

export default class Index extends Component {
    render() {
        return (
            <div>
                    <BrowserRouter>
                        <Switch>
                            <Route path="/" component={Top} />
                        </Switch>
                    </BrowserRouter>
            </div>
        );
    }
}

予約機能の作成

コードが長くてみにくいので必要な部分をピックアップして解説していく。

下記のコードにあるconstructor内では、stateを定義したり関数をバインドする。
これをしないとstate・関数が使えない。

constructor(props) {
super(props)
this.state = {
date: "",
}
this.onDateChange = this.onDateChange.bind(this);
}

下記のコードは、stateであるdateにフォームで入力された値を格納している。
onDateChange(e) {
this.setState({ date: e.target.value })
}

下記のコードのdisabled={this.state.isDisabled}はisDisabledのstateがtrueの場合はsubmitできないようにしている。
onClick={this.postReservation}は、submitした際にpostReervationメソッドが実行される。
postReservationメソッドについての説明はこの後に行う。
<Button className="float-right" id="btn" variant="contained" disabled={this.state.isDisabled} onClick={this.postReservation} color="primary">
送信する
</Button>

下記のコードの解説をする。
変数dataにフィールドに入力された値を格納する。
axiosを使用して非同期通信を行なっている。post送信で/reservationに対して先ほど定義した変数dataを送っている。
では。送信した先の/reservationを見ていく。

    const data = {
            date: this.state.date,
        }

            axios.post('/reservation', data)

先ほど解説した通りに非同期通信を行うと下記の処理が実行される。
先ほど定数dataで送った値が$requestに格納されている。
ReservationSendmailクラスをインスタンス化する際に$date渡す。

ReservationController.php
<?php

namespace App\Http\Controllers;

use App\Mail\ReservationSendmail;
use Illuminate\Support\Facades\Mail;
use App\Http\Requests\ReservationRequest;

class ReservationController extends Controller
{
    public function store(ReservationRequest $request)
    {
        $date = $request->date;
        $to = 'test@gmail.com';
        Mail::to($to)->send(new ReservationSendmail($date);

        return;
    }
}

次はReservationSendmailクラスをみる。

ReservationSendmail.php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ReservationSendmail extends Mailable
{
    use Queueable, SerializesModels;

    private $date;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct($date)
    {
        $this->date = $date; //ReservationControllerから送られてきた$dateをプロパティである$this->dateに格納している。
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this
            ->from('tatataabcd@gmail.com') //送信元のメールアドレス
            ->subject('自動送信メール') //メールのタイトル
            ->view('reservation.mail') //viewはreservation.mail.blade.phpを使用
            ->with([
                'date' => $this->date,  //先ほど格納したプロパティをreservation.mail.blade.phpでも使えるようにしている
            ]);
    }
}

次にreservation.mail.blade.phpを見る

mail.blade.php
お問い合わせ内容を受け付けました。<br>
<br>
■日時<br>
{!! $date !!}<br> この内容がReservationControllerのtoに指定したメールアドレスに送信される。
Reservation.js
import React, { Component } from 'react';
import moment from 'moment';
import Header from './Header';
import { Button } from '@material-ui/core';
import '../../../public/css/reservation.css';

class Reservation extends Component {
    constructor(props) {
        super(props)
        this.state = {
            date: "",
            name: "",
            phone: "",
            email: "",
            age: "",
            state: "",
            gender: "",
            isDisabled: false,
            errors: {
                date: [],
                name: [],
                phone: [],
                email: [],
                age: [],
                state: [],
                gender: [],
            }
        }
        this.onDateChange = this.onDateChange.bind(this);
        this.postReservation = this.postReservation.bind(this);
    }

    onDateChange(e) {
        this.setState({ date: e.target.value })
    }

    postReservation(e) {
        if (this.state.date !== "" && this.state.name !== "" && this.state.phone !== "" && this.state.email) {
            this.setState({
                isDisabled: true
            });
        }

        axios
            .post('/reservation', data)
            .then(response => {
                alert('予約を受け付けました。')
                this.setState({
                    isDisabled: false
                });

                this.setState({
                    errors: []
                });
            })
            .catch(error => {
                this.setState({
                    isDisabled: false
                });
                console.log(error.response.data.errors);
                const errors = this.state.errors;

                // 次の日から
                const nextDay = moment().add('1', 'd').format('YYYY-MM-DD');
                let reservationDay = this.state.date;
                reservationDay = reservationDay.slice(-16, -6);

                // 水木のみ
                const date = moment(this.state.date);
                const dayOfWeek = date.day();

                // 時間指定
                const hour = date.hour();
                const minute = date.minute();
                const hourAndMinute = hour + ':' + minute;

                if (this.state.date === "" ||
                    nextDay > reservationDay ||
                    dayOfWeek !== 3 &&
                    dayOfWeek !== 4 ||
                    hourAndMinute !== 10 + ':' + 0 &&
                    hourAndMinute !== 11 + ':' + 0 &&
                    hourAndMinute !== 13 + ':' + 0 &&
                    hourAndMinute !== 14 + ':' + 0 &&
                    hourAndMinute !== 15 + ':' + 0 &&
                    hourAndMinute !== 15 + ':' + 30 &&
                    hourAndMinute !== 16 + ':' + 0 &&
                    hourAndMinute !== 16 + ':' + 30
                ) {
                    errors.date = error.response.data.errors.date[0];
                } else {
                    errors.date = "";
                }
                this.setState({
                    errors: errors
                });

                if (this.state.name === "") {
                    errors.name = error.response.data.errors.name[0];
                } else {
                    errors.name = "";
                }
                this.setState({
                    errors: errors
                });

                if (this.state.phone === "" || isNaN(this.state.phone)) {
                    errors.phone = error.response.data.errors.phone[0];
                } else {
                    errors.phone = "";
                }
                this.setState({
                    errors: errors
                });

                if (this.state.email == "" || this.validateEmail(this.state.email)) {
                    errors.email = error.response.data.errors.email[0];
                } else {
                    errors.email = "";
                }
                this.setState({
                    errors: errors
                });
            });
    }

    render() {
        return (
            <React.Fragment>
                <Header />
                <div className="container">
                    <div className="row">
                        <div className="col-12">
                            <h1 class="h3 mb-5 mt-5 text-center">予約フォーム</h1>
                            <div className="form-group">
                                <span>予約日時</span>
                                <input className="form-control" type="datetime-local" name="date"
                                    value={this.state.date} onChange={this.onDateChange} />
                                <p className="err-msg">{this.state.errors.date}</p>
                            </div>


                            <Button className="float-right" id="btn" variant="contained" disabled={this.state.isDisabled} onClick={this.postReservation} color="primary">
                                送信する
                            </Button>
                        </div>
                    </div>
                </div>
            </React.Fragment >
        );
    }
}

export default Reservation;

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

シンプルGatsbyサイトにガンガン機能を追加する(前編)

前に作ったGatsbyブログをアップグレードしよう!

gatsby入門 ブログ作ってサーバーにアップしてみるで作ったサイトをアップグレードします。
結論から言うとプラグインを入れたり、graphqlを工夫したりすれば割と早く機能追加は可能でした。

前回は。。。

前回作成してアップしたサイトはこんな感じのシンプルなブログでした。
2020-09-08_00h43_41.jpg

そして今回

今回作成したのがこれです。
2020-09-13_03h15_25.jpg
割と頑張った!

追加した機能

今回以下機能を追加しました。

  1. レスポンシブ対応
  2. マークダウン記事をブログ・ワークに区分けしそれぞれを別のカテゴリとして表示(BLOGとWORKS)
  3. 最新記事6件を表紙に表示
  4. ページングを含むブログ一覧表示
  5. 自己紹介は作成したが未完

レスポンシブ対応

これはgatsby-plugin-typographyとMaterial-UIに全て頼っています。
gatsby-plugin-typographyでは文字サイズやpadding等の幅やの設定を任せています。
Material-UIでヘッダーやアイコンなどを作成しています。
例えばヘッダーの動作は以下となっています。

header.js(一部)
  ※レスポンシブ対応の部分のみ記載
  const useStyles = makeStyles((theme) => ({
    menuLinkBox: {
      [theme.breakpoints.up('sm')]: {
        display: 'block', ←画面サイズがsmより上ならmenuLinkBoxを表示
      },
      [theme.breakpoints.down('sm')]: {
        display: 'none', ←画面サイズがsmより下ならmenuLinkBoxを非表示
      },
    },
    ↑↑↑↑↑
    画面サイズによりどちらかが表示される状態にしている
    ↓↓↓↓↓
    menuLinkBoxSm: {
      [theme.breakpoints.up('sm')]: {
        display: 'none', ←画面サイズがsmより上ならmenuLinkBoxSmを非表示
      },
      [theme.breakpoints.down('sm')]: {
        display: 'block', ←画面サイズがsmより上ならmenuLinkBoxSmを表示
      },
    },
  }));

      <AppBar position='static'>
        <Toolbar>
          <Box>
            <Link to={`/`}>
              <Image/>
            </Link>
          </Box>
          <Box className={styleClass.menuLinkBox}>
            <Link to={`/`}>
              HOME
            </Link>
            ・・・省略・・・
          </Box>
          <Box className={styleClass.menuLinkBoxSm}>
            <IconButton>
              <MenuIcon />
            </IconButton>
            <Menu>
              <MenuItem>
                <Link to={`/`}>
                  HOME
                </Link>
              </MenuItem>
              ・・・省略・・・
            </Menu>
          </Box>
        </Toolbar>
      </AppBar>

2020-09-13_03h39_09.jpg
2020-09-13_03h42_20.jpg
あまりレスポンシブに重きを置くとリリースが遅れると考えたので、レスポンシブの基準は以下2点のみに絞りました。

  • theme.breakpoints.up('sm')
  • theme.breakpoints.down('sm')

業務アプリでレスポンシブ対応は、あまり必要なくて気にしたことなかったけど、これ細かく考えだすとめちゃくちゃ時間かかりそうですね。

マークダウン記事をブログ・ワークに区分けしそれぞれを別のカテゴリとして表示(BLOGとWORKS)

最新記事6件を表紙に表示

記事の区分けはマークダウンの文章にタグ:kindをつけることで対応しました。
2020-09-13_03h59_50.jpg
2020-09-13_04h00_40.jpg
こうすることでgraphqlで分けて取得することが可能になります。

index.js
import React from "react";
import {useStaticQuery, graphql } from "gatsby";

import Layout from "../components/layout";
import SEO from "../components/seo";
import PostList from "../components/postList";

const BlogIndex = ({location}) => {
  const indexData = useStaticQuery(graphql`
    query IndexDataQuery {
      blog: allMarkdownRemark(filter: {frontmatter: {kind: {regex: "/blog/"}}}, sort: {fields: [frontmatter___date], order: DESC}, limit: 6) {
      ↑同一項目を複数回取得する場合、先頭にblog:(別名 + :)と記載し、別名を設定
      ↑{frontmatter: {kind: {regex: "/blog/"}}}によりblogの記事が取得できます。
      ↑limit: 6で表紙に出力する最新6件と定義
        edges {
          node {
            excerpt
            fields {
              slug
            }
            frontmatter {
              date(formatString: "YYYY年MM月DD日")
              title
              description
              kind
            }
          }
        }
      }
      work: allMarkdownRemark(filter: {frontmatter: {kind: {regex: "/work/"}}}, sort: {fields: [frontmatter___date], order: DESC}, limit: 6) {
      ↑{frontmatter: {kind: {regex: "/work/"}}}によりworkの記事が取得できます。
        edges {
          node {
            excerpt
            fields {
              slug
            }
            frontmatter {
              date(formatString: "YYYY年MM月DD日")
              title
              description
              kind
            }
          }
        }
      }
    }
  `);

  const blogs = indexData.blog.edges;←別名blogで取得
  const works = indexData.work.edges;←別名workで取得
  return (
    <Layout location={location}>
      <SEO title="Top" />
      <PostList title="BLOG" listUrl="/blog" postList={blogs}/>
      <PostList title="WORKS" listUrl="/works" postList={works}/>
    </Layout>
  )
}

export default BlogIndex

上記にでblog、workで分割して表示できます。
ゆくゆくはタグ検索のような機能が欲しいな。

今回はここまでです。

少しお知らせ

今回作成したブログは以下で見ることができます!!
https://3s-laboo.com/

コンテンツは今作成中ですので少しづつ数を増やしていく予定です!
よろしくお願いします!

また、上記サイトのプログラムは以下に保存しています。
https://github.com/3sLaboo/gatsby-blog-3SLaboo

基本的な機能はそろえたのでテンプレートにでもぜひ使ってください!
ありがとうございました!

gatsbyの作業履歴

gatsby入門 チュートリアルをこなす 0.開発環境をセットアップする
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(1)
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(2)
gatsby入門 チュートリアルをこなす 2. ギャツビーのスタイリングの概要
gatsby入門 チュートリアルをこなす 3. ネストされたレイアウトコンポーネントの作成
gatsby入門 チュートリアルをこなす 4. ギャツビーのデータ
gatsby入門 チュートリアルをこなす 5. ソースプラグインとクエリされたデータのレンダリング
gatsby入門 チュートリアルをこなす 6. 変圧器プラグイン※Transformer pluginsのgoogle翻訳
gatsby入門 チュートリアルをこなす 7. プログラムでデータからページを作成する
gatsby入門 チュートリアルをこなす 8. 公開するサイトの準備
gatsby入門 ブログ作ってサーバーにアップしてみる

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