20200517のReactに関する記事は10件です。

React(gatsby.js)でhighlight.jsを使う

実装

highlight.jsを追加する。今回はES6以上で書きます。
さらに初歩的なところを知りたい方はこちらのブログを読んでください

// npmモジュール追加
yarn add highlight.js

今回はコードハイライトを使用する機会が記事のページでしか使わないため、Util的なものは実装せずに直接該当のReact Componentに突っ込む。
useEffectがわからない方はこちらを確認してください。

Article/index.jsx
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import 'highlight.js/styles/atom-one-dark.css';

hljs.registerLanguage('javascript', javascript);

const ArticleComponent = () => {
    useEffect(() => {
        hljs.initHighlighting();
    });
    return <Article />;
}

ここではhljs.initHighlighting()を使うようにしてください。
hljs.initHighlightingOnLoad()の実態はDOMContentLoaded時にinitHighlighting()を呼び出すようにaddEventListenerするだけのものです。
しかし、おそらくReactの仕組み上、要素がアクセス可能になるタイミング、いわゆるマウント済みになるタイミング(componentDidMount)とDOMContentLoadedのタイミングが同じではないため、リスナーがあってもハイライト処理がReactのマウント時より前に走ってしまうようです。
なので、呼び出す際は上記のようにReactのマウント時にinitHighlightingを実行するようにします。
するとこんな感じでハイライトしてくれます。何となくよくあるハイライトだと思ったのでatom-one-dark.cssを選んでます。こちらの例のスタイルがそれになります。
highlight-js-demo.png
また、今回の実装だと初回時のみにハイライトをしてくれますが、初回以降上記のコードだとハイライトしてくれなくなります。これはhighlight.jsのインスタンスが初期化以降は処理を走らせないようにフラグを用いてテキストのパース処理をスキップしているためです。
今回は初回以降もパースしてもらわないといけない要件なのでフラグを折りに行きます。

Article/index.jsx
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import 'highlight.js/styles/atom-one-dark.css';

hljs.registerLanguage('javascript', javascript);

const ArticleComponent = () => {
    useEffect(() => {
        hljs.initHighlighting();
        // React環境だと初回以降ハイライト処理が入らないため外部からフラグをfalseに
        hljs.initHighlighting.called = false;
    });
    return <Article />;
}

こんな感じに実装すると初回以降もハイライトしてくれるようになります。
アウトローな実装になりましたが、今回はこんなところで問題なく動いているので開発を終了しました。

公式ドキュメント

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

RailsとReactをGraphQLでつなぐ

前回の記事でdocker-composeを使ってRailsとReactを立ち上げることができたので今回はこれらをGraphQLでつなぐところまでやっていきます!

Rails側のGraphQL設定

まずはRailsをGraphQLにつなぎます
※Rails側の設定についてはこちらの記事を参考にさせていただきました

最初にGraphQLのgemをインストールします

Gemfile
gem 'graphql'

group :development do
  gem 'graphiql-rails'
end

これで一回bundle install

GraphQLが入ったのでrails g graphql:installで必要なファイルが一通り作られます

APIモードでは一部追加されない部分があるので追加します

config/route.rb
if Rails.env.development?
  mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end
config/application.rb
require "sprockets/railtie"
app/assets/config/manifest.js
//= link graphiql/rails/application.css
//= link graphiql/rails/application.js

最後にReact側からの接続を有効にするためにapplication.rbに以下を追記します。こちらに関してはまだ理解が足りてないのでまた改めて記事を...!

config/application.rb
config.hosts = nil
config.autoloader = :classic

これでRails側の設定は完了です!

docker-compose up -dをして、http://localhost:3000/graphiqlにアクセスするとgraphiqlの画面がでてきます

左側に

query{
  testField
}

と書いて▶ボタンを押してみましょう

右側に

{
  "data": {
    "testField": "Hello World!"
  }
}

と出てきたら成功です!

Rails-GraphQL.png

React側のGraphQL設定

ReactでもGraphQLを設定していきます

まずパッケージのインストールを行います

package.jsonのdependenciesに以下を追加します(バージョンは記事を書いたときのものなので適宜変更をおねがいします)

"@apollo/react-hooks": "3.1.5",
"apollo-boost": "0.4.9",
"apollo-client": "^2.6.4",
"graphql": "^14.3.1",
"graphql-tag": "2.10.3",
"react-apollo": "3.1.5",
"react-router-dom": "5.2.0",

※apollo-boostは他のもので代用可能です。apollo-boostはカスタマイズ性に乏しいらしいので人によっては使わないほうがいいかもしれないです。詳しくはこちらの記事ドキュメントをご覧ください

また、RailsとReactをつなぐためにproxyを使います。

package.json
"proxy": "http://rails:3000"

proxyというのは英語で代理人という意味でhttp://localhost:4000に来たリクエストをhttp://rails:3000にそのまま渡します。rails:3000というのはdocker-composeで作成したrailsコンテナの3000番ポートのことなのでこれでReactに来たGraphQLのリクエストをRails側に送ることができます

※このようにproxyを設定すると他のパスもすべてRailsに送られてしまうのではないかと思いますがリクエストで送る内容に応じてうまくproxyするものを選んでくれるみたいです(この辺そんなに詳しくないのでちゃんとわかったらまたどこかで書こうと思います)

接続の準備ができたので早速実装していこうと思います

src/App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import ApolloClient from 'apollo-boost'
import { ApolloProvider } from "@apollo/react-hooks"
import Top from './pages/Top';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql'
})

class App extends Component {
  render() {
    return (
      <ApolloProvider client={client} >
        <div className="App">
          <Router>
            <Route exact path='/' component={Top}/>
          </Router>
        </div>
      </ApolloProvider>
    );
  }
}

export default App;
src/pages/Top.js
import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';

const TEST = gql`
  query test {
    testField
  }
`;

export default function Top() {
  const { loading, data } = useQuery(TEST, {});
  if (loading) return <p>Loading ...</p>;
  return <h1>{data.testField}</h1>;
}

これで設定は完了です!docker-compose up -dしてhttp://localhost:4000にアクセスしてみましょう。下の画面のようにでてくれば成功です!

React-GraphQL.png

以上でGraphQLでRailsとReactをつなぐ方法は終わりです!

まだまだ細かいところで理解不足なところが多いのでそこは引き続き勉強して拡充していければと思います

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

RecoilをRxJSで再実装する

動機

  • Recoilを見ていて、Atom/Selectorという単位を用意した上での非同期更新伝播の仕組みが『自由度をうまく狭めたRx』だなあ、という印象を受けた。
  • APIとしては React & Hooks環境が大前提になってるようだけど、上記の印象があるので、理想的には特にReact & Hooksべったりじゃなくてもいいのでは(Reduxと同様にstandaloneで利用できるのでは)と考えた。
  • 現状のRecoilのソースを見たら、State管理のライブラリにいきなりReactDOMが出てきたりして辛いので、あまり見てはいけない気がした(型定義だけはとりあえず信用する)。

基本方針

  • atom/selectorの結果得られるRecoilStateを、RxJSのstreamをwrapするオブジェクトとして実装する。各stateの値の更新はstreamに流される
  • RecoilState はhooks以外でも使えるよう、内包するstreamをsubscribeできるようにしている
  • useRecoilState/useRecoilValueなどのhooksのAPIから、RecoilStateの現在の値の取得および非同期未解決の場合はPromise値をthrowする関係上、解決された値を取得できるだけでなく非同期処理(Promise)が実行中か実行後かを分かる必要がある。
    • streamにはLoadableという名前の「状態(isLoading/hasValue/hasError)と値/Promiseのペア」を流す
    • RxJSではBehaviorSubjectを使うことで最後にstreamに流れた値をいつでも取得できる
  • 各hooks(useRecoilState/useRecoilValue)は、useState()でローカルのstateを保持し、useEffect()内でstreamをsubscribeする。streamから値が流れてきたらその値でローカルstateを更新する(これによりReactで再描画が発生する)

実装

recoil-rxjs/recoil.ts
import { SetStateAction } from "react";
import { BehaviorSubject } from "rxjs";
import { skip } from "rxjs/operators";

/**
 *
 */
function getNextState<T>(action: SetStateAction<T>, getPrevState: () => T) {
  if (typeof action === "function") {
    const prevState = getPrevState();
    return (action as ((prevState: T) => T))(prevState);
  }
  return action;
}

/**
 * 
 */
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
  return typeof (value as any)?.then === 'function';
}

/**
 * this interface is not in official recoil, but introduced for the ease of implementation.
 */
export interface RecoilState<T> {
  key: string;
  getValue: () => T;
  setValue: (action: SetStateAction<T>) => void;
  getPromise: () => Promise<T>;
  subscribe: (listener: (value: T) => void) => () => void;
}

/**
 * global states
 */
const states = new Map<string, RecoilState<any>>();

/**
 *
 */
type AtomInput<T> = {
  key: string;
  default: T;
};

/**
 *
 */
export function atom<T>(input: AtomInput<T>): RecoilState<T> {
  const { key, default: defaultValue } = input;
  let state: RecoilState<T> | undefined = states.get(key);
  if (state) {
    return state;
  }
  const stream$ = new BehaviorSubject<T>(defaultValue);
  state = {
    key,
    getValue() {
      return stream$.value;
    },
    setValue(action: SetStateAction<T>) {
      const value = getNextState(action, () => this.getValue());
      stream$.next(value);
    },
    getPromise() {
      return Promise.resolve(stream$.value);
    },
    subscribe(listener: (value: T) => void) {
      const sbscr = stream$.pipe(skip(1)).subscribe(listener);
      return () => sbscr.unsubscribe();
    }
  };
  states.set(key, state);
  return state;
}

/**
 *
 */
type SelectorGetHelper = {
  get<T>(state: RecoilState<T>): T;
  getPromise<T>(state: RecoilState<T>): Promise<T>;
};

type SelectorSetHelper = {
  get<T>(state: RecoilState<T>): T;
  set<T>(state: RecoilState<T>, value: T): void;
};

type SelectorInput<T> = {
  key: string;
  get: (helper: SelectorGetHelper) => T | Promise<T>;
  set?: (helper: SelectorSetHelper, value: T) => void;
};

type LoaderState<T> =
  | {
      status: "isLoading";
      promise: Promise<T>;
    }
  | {
      status: "hasValue";
      contents: T;
    }
  | {
      status: "hasError";
      contents: any;
    };

/**
 *
 */
export function selector<T>(input: SelectorInput<T>): RecoilState<T> {
  const { key, get: getValue, set: setValue } = input;
  let state: RecoilState<T> | undefined = states.get(key);
  if (state) {
    return state;
  }
  let stream$: BehaviorSubject<LoaderState<T>> | undefined = undefined;
  const toLoaderState = (value: T | Promise<T>): LoaderState<T> => {
    return isPromise(value) ? {
      status: "isLoading",
      promise: value
    } : {
      status: "hasValue",
      contents: value,
    };
  };
  const isLoaderStateInSamePromise = (promise: Promise<T>) => {
    if (!stream$) {
      return false;
    }
    const currentLoader = stream$.value;
    return currentLoader.status === 'isLoading' && currentLoader.promise === promise;
  };
  const handleLoaderStateUpdate = (loader: LoaderState<T>) => {
    stream$?.next(loader);
    if (loader.status === 'isLoading') {
      const promise = loader.promise;
      promise.then(
        value => {
          if (isLoaderStateInSamePromise(promise)) {
            stream$?.next({
              status: "hasValue",
              contents: value
            });
          }
        },
        error => {
          if (isLoaderStateInSamePromise(promise)) {
            stream$?.next({
              status: "hasError",
              contents: error
            });
          }
        }
      );
    }
  };
  const createLoaderStateStream = () => {
    const value = getValue(getHelper);
    const loader = toLoaderState(value);
    return new BehaviorSubject<LoaderState<T>>(loader);
  };
  const updateState = () => {
    if (!stream$) {
      stream$ = createLoaderStateStream();
    } else {
      const value = getValue(getHelper);
      const loader = toLoaderState(value);
      handleLoaderStateUpdate(loader);
    }
  };
  const deps: Set<string> = new Set();
  const getHelper = {
    get<U>(state: RecoilState<U>) {
      if (!deps.has(state.key)) {
        state.subscribe(updateState);
        deps.add(state.key);
      }
      return state.getValue();
    },
    getPromise<U>(state: RecoilState<U>) {
      if (!deps.has(state.key)) {
        state.subscribe(updateState);
        deps.add(state.key);
      }
      return state.getPromise();
    }
  };
  const setHelper = {
    get<U>(state: RecoilState<U>) {
      if (!deps.has(state.key)) {
        console.log("subscribing", key, state.key);
        state.subscribe(updateState);
        deps.add(state.key);
      }
      return state.getValue();
    },
    set<U>(state: RecoilState<U>, value: U) {
      return state.setValue(value);
    }
  };
  state = {
    key,
    getPromise() {
      if (!stream$) {
        stream$ = createLoaderStateStream();
      }
      const loader = stream$.value;
      if (loader.status === "isLoading") {
        return loader.promise;
      }
      if (loader.status === "hasError") {
        return Promise.reject(loader.contents);
      }
      return Promise.resolve(loader.contents);
    },
    getValue() {
      if (!stream$) {
        stream$ = createLoaderStateStream();
      }
      const loader = stream$.value;
      if (loader.status === "isLoading") {
        throw loader.promise;
      }
      if (loader.status === "hasError") {
        throw loader.contents;
      }
      return loader.contents;
    },
    setValue(action: SetStateAction<T>) {
      if (setValue) {
        const value = getNextState(action, () => this.getValue());
        setValue(setHelper, value);
      }
    },
    subscribe(listener: (value: T) => void) {
      if (!stream$) {
        stream$ = createLoaderStateStream();
      }
      const sbscr = stream$.subscribe(loader => {
        if (loader.status === "hasValue") {
          listener(loader.contents);
        }
      });
      return () => sbscr.unsubscribe();
    }
  };
  states.set(key, state);
  return state;
}
recoil-rxjs/hooks.ts
import {
  useState,
  useCallback,
  useEffect,
  useMemo,
  SetStateAction
} from "react";
import { RecoilState } from "./recoil";

/**
 *
 */
export function useRecoilState<T>(state: RecoilState<T>) {
  const [value, setRawValue] = useState(state.getValue());
  const setValue = useCallback(
    (action: SetStateAction<T>) => {
      state.setValue(action);
    },
    [state]
  );
  useEffect(() => {
    return state.subscribe(setRawValue);
  }, [state]);
  return [value, setValue] as const;
}

/**
 *
 */
export function useRecoilValue<T>(state: RecoilState<T>) {
  const [value, setRawValue] = useState(state.getValue());
  useEffect(() => {
    return state.subscribe(setRawValue);
  }, [state]);
  return value;
}

/**
 *
 */
export function useSetRecoilState<T>(state: RecoilState<T>) {
  const setValue = useCallback(
    (action: SetStateAction<T>) => {
      state.setValue(action);
    },
    [state]
  );
  return setValue;
}

/**
 *
 */
type CallbackInterface = {
  getPromise<U>(state: RecoilState<U>): Promise<U>;
  set<U>(state: RecoilState<U>, action: SetStateAction<U>): void;
};

export function useRecoilCallback<Args extends any[], R>(
  callback: (helper: CallbackInterface, ...args: Args) => R,
  deps: any[]
) {
  const helper = useMemo(
    () => ({
      getPromise<U>(state: RecoilState<U>) {
        return state.getPromise();
      },
      set<U>(state: RecoilState<U>, action: SetStateAction<U>) {
        return state.setValue(action);
      }
    }),
    []
  );
  return useCallback(
    (...args: Args) => {
      return callback(helper, ...args);
    },
    [helper, ...deps] // eslint-disable-line
  );
}

コード

Codesandboxで実際に動くやつ。アプリはuhyoさんの記事つかっていたやつから拝借。
https://codesandbox.io/s/stoic-drake-2f64z

所感

  • 各selectorでmemoizationの必要があるとおもうけど、省いている(ただしそこもRxJSでできるはず)。
  • RecoilRootはこの実装では必要なかったやつ。そもそもglobalにatom/selectorを定義しちゃうのに階層でstoreを局所化する意味とは何だ?storeの構成は共通でもstoreのstateは別々にしたい、というユースケースがあるのかどうか?
  • なお久々にRxJS使ったらstreamにメソッドが生えているのではなく.pipe()でoperationをつなぐようになっていて、バンドルサイズ小さくなるよう工夫されてるなと思った
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[deck.gl]React x TypeScript x DeckGL Part1

Nx(React)ワークスペースにdeck.glの環境構築を行った際の備忘録。
本記事ではReactコンポーネントの生成まで行う。

実際の導入編はこちら
[deck.gl]React x TypeScript x DeckGL Part2

事前準備

  • node.js 12.16.3(LTS)

Nxのワークスペースを作成

ReactのボイラープレートにNxを利用する。

  • Nxは本来モノレポを実現するための開発ツールセット
  • Mockサーバや共通ライブラリの作成が容易
  • 最初からTypeScriptが使える
$ npx create-nx-workspace@lates
? Workspace name (e.g., org name)     my-workspace
? What to create in the new workspace react
? Application name                    deck-gl-app
? Default stylesheet format           SCSS

コミット時にフォーマッタを実行

Gitフックが簡単に設定できるhuskyを利用する。

$ npm install hasky --save-dev

package.jsonにhuskyの設定を記述

package.json
{
  ...
  "husky": {
    "hooks": {
      "pre-commit": "npm run format:write"
    }
  }
}

Nxコマンドをインストール(任意)

Reactコンポーネントを@angular/cliっぽく自動生成したいのでnxコマンドをインストール。
※グローバルインストールを避けたい場合は毎回npx nxとすれば良い。

$ npm install -g @nrwl/cli

Mapboxをインストール

MapboxGLのReactコンポーネントを提供しているreact-map-glを利用する。
※TypeScriptで書くので型定義ファイルのインストールが必要

$ npm install --save react-map-gl @types/react-map-gl

DeckGLをインストール

本題のdeck.glをインストールします。型定義ファイルは@typeではなく@danmarshall/deckgl-typingsから提供されている。

$ npm install deck.gl --save
$ npm install @danmarshall/deckgl-typings --save

動作確認

ローカルサーバーを起動し、表示の確認を行う。
※Nxの初期画面が表示されればOK

$ npm start

Reactコンポーネントを作成

@nx/cliを使ってReactコンポーネントを自動生成する
@angular/cligenerateコマンドのように、コンポーネント、スタイルシート、テストコードの雛形がそれぞれ生成される

$ nx g @nrwl/react:component my-map --project=deck-gl-app

次回は生成したコンポーネントにMapboxの地図とdeck.glを利用したレイヤーをいくつか実装する。

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

初心者でもServerless Next.js+Auth0でログイン機能をもったserverlessなサイトが簡単に作れた話(1)

何でも見てやろう』(小田実)の精神で、自分でもコードを書いてみる編集者・長尾です。
KODANSHAtech LLC.でゼネラルマネージャーやってます。
エンジニアのみなさん大募集中なので、メディアやコンテンツ開発に興味のある方はぜひ〜。
実はAuth0 Ambassadorもやってます。

さて、今回はとっても便利なServerless Next.js1に、とっても便利なIdaaS、Auth0を組み合わせて、簡単便利にサーバレスな会員制サイトを作れそうということで、実験してみた一連の流れをご紹介してみます2

少し書いてみたら結構ボリューミーだったので、以下のように3回に分けようと思います。

  1. セッティングとユーザーのログイン状態を取り回せる基本的なページの構築
  2. SSR時にユーザー情報を必要とすることで、そもそも非ログインユーザーには表示できないページを作る方法
  3. 外部のAPIにauth tokenを送って検証させる方法

元ネタは以下になります。
とくにauth0/nextjs-auth0のexampleをなぞる部分が多くなっています。
- https://github.com/auth0/nextjs-auth0
- https://github.com/danielcondemarin/serverless-next.js

やってみた記事なので、変なところがあったら、ぜひご指摘ください…。

前提

serverless/Auth0など利用するリソースはこちら(折りたたんでいます)

このあたりまでは、すでに利用されている方向けです。
Go Serverless!

も使っていきます。
そしてもちろん、IdaaSとしては、我らがAuth0です。

serverlessでもいろいろなクラウドサービス(SaaS)が利用できますが、本記事ではAWSを利用する形で考えていきます。

Serverless Next.js

serverless-nextjs-logo.gif

ServerlessBlog: https://www.serverless.com/blog/serverless-nextjs/
GitHub: https://github.com/danielcondemarin/serverless-next.js

クラウドリソースをベースに、サーバを必要とせず、サービスを提供できるServerless Framework。
AWSコンソールをポチポチやらなくても、 serverless.ymlに必要な記述をすれば、 CloudFormationによってリソースが立ち上がってくれる、とっても便利なframeworkですよね。

そんなserverlessの世界をさらに豊かにするために作られたのが、 serverless component
ごくかいつまんでいうと、serverlessで構築されるサービスを、 Componentという塊として定義し、再利用可能にしたブロックのようなものです。

参考: https://www.serverless.com/blog/what-are-serverless-components-how-use/

Serverless Next.jsは、簡単設定でNext.jsのserverless modeが立ち上がるというすぐれものです。

筆者は初学者なので、充実した解説は他の方にお願いするとして、大きな特徴だと感じたのは、 Serverless Next.jsがLambda@Edgeでホスティングされるという点です。そのため、

  • pages のServerSideRendering(SSR)がLambda@Edge上で行われる。
  • APIも同じくLambda@Edgeで取り回され、実行される。

これはつまり、 CloudFrontのEdgeですべてが行われるということで、もはや特定のregionに依存しない形でNext.jsを利用したサービスを提供できることを意味します(この点は、後述する設定の仕方に少し影響しています)。

余談ですが、KODANSHAtech LLC.のサイトもServerless Next.jsで作ってみたサイトです。

Auth0

"Identity is Complex. Deal with it."

いわゆるログイン機能、あるいは認証認可の機能を提供するサービスといえば、 CognitoFirebase Authenticationなど、さまざまな選択肢があります。

そんな中、認証認可基盤であることに完全に特化することで、APIを通じた「疎」な世界観にマッチしたIdaaSとして注目されているのが、Auth0です。

どんなサービスにも組み込みやすく、ソーシャルログインやOIDC、SAMLへの対応も簡単、しかもドキュメンテーションが非常に充実していて、何をどう取り回せばセキュアなID管理ができるのか、すぐに調べられるのも大きな特徴でしょう。

この記事の末尾で触れたいと思いますが、Next.jsをserverlessで利用する場面では、やはりAuth0が「疎」なIdaaSを志向していることが、強みを発揮すると思います。

いろいろ書きたくなるのですが、「やってみた」パートに早く移るため、詳細は下記のリンクを示すことで代えさせていただきます!

Getting Started

今回は、Serverless Next.jsにAuth0 Next.jsの公式exampleを組み合わせて、ログイン機能を実現し、ユーザー情報を表示させるところまでを試してみます。

まずは、Serverless Next.jsの準備からです。

Preparing serverless-next.js

terminal
mkdir my-project
cd my-project
npm init -y
npm install --save-dev serverless-next.js
touch .env
touch serverless.yml

serverless.ymlには、よくServerless Frameworkで書くようにproviderとかresoucesといったことを列記する必要はなく、下記の記述だけで事足ります。

serverless.yml
myNextApplication:
  component: serverless-next.js

実際の利用の場面では、サービスを独自のドメインで公開することになると思います。
その場合、Route53を利用して設定するドメインの指定をここに記述します。

serverless.yml
myNextApplication:
  component: serverless-next.js
  inputs:
    domain: "example.com"

サブドメインを利用する場合は、次のようになります。

serverless.yml
myNextApplication:
  component: serverless-next.js
  inputs:
    domain: ["sub","example.com"]

ここで注意が必要なのは、ドメインのCertificateについてです。
利用したいドメインについての証明書は、deployの前に取得しておく必要があります。
しかし、上述したように、Serverless Next.jsはLambda@Edgeでホスティングされるため、

もはや特定のregionに依存しない形でNext.jsを利用したサービスを提供できる

ものになっています。
そのため、利用するドメインに対するcertificateは、Lambda@Edgeが利用可能なus-east-1で取得しておく必要があります。
AWSのCertificate Managerで証明書を取得するときは、リージョンに注意してください。

.envには、AWSのcredentialsを入れておきます。適宜、ご利用のものに入れ替えてください。

.env
AWS_ACCESS_KEY_ID=accesskey
AWS_SECRET_ACCESS_KEY=accesssecret

これはserverless-next.jsのために必要だという設定ではありませんが、あとで利用するために、package.jsonscriptsは以下のようにしておきます。

package.json
{
...
 "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start -p $PORT",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...
}

Preparing Auth0 Application

アカウントの作成などは、前述した他の記事をご参照ください。
ここでは、Auth0に新しいtenantを作り、Applicationsのタブを開きます。

Domain/ClientID/ClientSecretの取得方法や設定など。画像が多いので折りたたんでいます。Click Here to Unfold!

スクリーンショット 2020-05-16 18.37.24.png

+CREATE APPLICATIONで、Regular Web Applicationsを選択し、新しいApplicationを作ります。

スクリーンショット 2020-05-16 18.42.10.png

作ったApplicationのSettingsタブで、以下の情報が確認できるので、これをメモしておきます。

  • Domain
  • Client ID
  • Client Secret

スクリーンショット 2020-05-16 18.44.01.png

また、Settingsの最下部に、Show Advanced Settingsがありますので、ここを開いて、OAuthのタブを確認しておきましょう。
JsonWebToken Signiture AlgorithmRS256OIDC Conformantはonになっている必要があります。

スクリーンショット 2020-05-16 18.47.25.png

次に、ローカルでNext.jsのアプリを立ち上げて試す場合に、Auth0が機能するように、Application URLsのセクションに、次のように記入しておきます。

スクリーンショット 2020-05-16 18.54.13.png

Preparing Next.js Application with Auth0

次に、Next.jsとAuth0でアプリケーションを作っていきます。

terminal
npm install --save next react react-dom
touch next.config.js

next.config.jsには、以下の記述を入れます。

next.config.js
module.exports = {
  target: "serverless"
};

今回は、Next.jsで簡単にAuth0が使えるauth0/nextjs-auth0を利用していきます。

https://github.com/auth0/nextjs-auth0

terminal
npm install --save @auth0/nextjs-auth0 dotenv isomorphic-unfetch
mkdir lib
touch lib/auth0.js
touch lib/auth0-config.js

ちょっとディレクトリの命名に迷うのですが、今回はlibの中にauth0.jsを作ります(ちなみに公式GitHubではREADMEの解説でutil、exampleで使われている実際のディレクトリはlib)。
auth0.jsは以下のように記述します。
公式のままだと、場合によって書き直して使わないといけないので、少し変更していますが、お好みにあわせてどうぞ。

auth0.js
import { initAuth0 } from '@auth0/nextjs-auth0';
import config from './auth0-config';

const auth0 = (opt) => {
  opt = opt || {};
  let params = {
    domain: config.AUTH0_DOMAIN,
    clientId: config.AUTH0_CLIENT_ID,
    clientSecret: config.AUTH0_CLIENT_SECRET,
    scope: opt.scope || config.AUTH0_SCOPE,
    redirectUri: opt.redirectUri || config.REDIRECT_URI,
    postLogoutRedirectUri: opt.postLogoutRedirectUri || config.POST_LOGOUT_REDIRECT_URI,
    session: {
      // The secret used to encrypt the cookie.
      cookieSecret: config.SESSION_COOKIE_SECRET,
      // The cookie lifetime (expiration) in seconds. Set to 8 hours by default.
      cookieLifetime: opt.session && opt.session.cookieLifetime ? opt.session.cookieLifetime : config.SESSION_COOKIE_LIFETIME,
      // (Optional) The cookie domain this should run on. Leave it blank to restrict it to your domain.
      // cookieDomain: config.SESSION_COOKIE_DOMAIN, //今回は使わないでおきます。
      // (Optional) SameSite configuration for the session cookie. Defaults to 'lax', but can be changed to 'strict' or 'none'. Set it to false if you want to disable the SameSite setting.
      cookieSameSite: 'lax',
      // (Optional) Store the id_token in the session. Defaults to false.
      storeIdToken: opt.session && opt.session.storeIdToken ? opt.session.storeIdToken : false,
      // (Optional) Store the access_token in the session. Defaults to false.
      storeAccessToken: opt.session && opt.session.storeAccessToken ? opt.session.storeAccessToken : false,
      // (Optional) Store the refresh_token in the session. Defaults to false.
      storeRefreshToken: opt.session && opt.session.storeRefreshToken ? opt.session.storeRefreshToken : false
    },
    oidcClient: {
      // (Optional) Configure the timeout in milliseconds for HTTP requests to Auth0.
      httpTimeout: opt.oidcClient && opt.oidcClient.httpTimeout ? opt.oidcClient.httpTimeout : 2500,
      // (Optional) Configure the clock tolerance in milliseconds, if the time on your server is running behind.
      clockTolerance: opt.oidcClient && opt.oidcClient.clockTolerance ? opt.oidcClient.clockTolerance : 10000
    }
  };
  if(opt.aud){
    params['audience'] = config.AUDIENCE
  }
  return initAuth0(params);
};

export default auth0;

auth0-config.jsは、公式exampleにあわせ、次のようにしています。

auth0-config.js
if (typeof window === 'undefined') {
   /**
    * サーバーサイドで使われるセッティング
    */
   module.exports = {
     AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
     AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
     AUTH0_SCOPE: process.env.AUTH0_SCOPE,
     AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
     REDIRECT_URI: process.env.REDIRECT_URI,
     POST_LOGOUT_REDIRECT_URI: process.env.POST_LOGOUT_REDIRECT_URI,
     SESSION_COOKIE_SECRET: process.env.SESSION_COOKIE_SECRET,
     SESSION_COOKIE_LIFETIME: process.env.SESSION_COOKIE_LIFETIME,
     //SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN //指定したい場合。以下略。
     AUDIENCE: process.env.AUDIENCE
   };
} else {
   /**
    * クライアントサイドに露出するセッティング
    */
   module.exports = {
     AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
     AUTH0_SCOPE: process.env.AUTH0_SCOPE,
     AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
     REDIRECT_URI: process.env.REDIRECT_URI,
     POST_LOGOUT_REDIRECT_URI: process.env.POST_LOGOUT_REDIRECT_URI
   };
}

次に、next.config.jsenvで情報を読み込みます。

next.config.js
const dotenv = require('dotenv');

dotenv.config();

module.exports = {
  target: "serverless",
  env: {
    AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
    AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
    AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET,
    AUTH0_SCOPE: 'openid profile',
    REDIRECT_URI: process.env.REDIRECT_URI || 'http://localhost:3000/api/callback',
    POST_LOGOUT_REDIRECT_URI: process.env.POST_LOGOUT_REDIRECT_URI || 'http://localhost:3000/',
    SESSION_COOKIE_SECRET: process.env.SESSION_COOKIE_SECRET,
    SESSION_COOKIE_LIFETIME: 7200, // 2 hours
    AUDIENCE: process.env.AUDIENCE
  }
};

最後に各種の情報を.envに追記します。
先ほど、Auth0のコンソールをみながらメモした情報です。

.env
AWS_ACCESS_KEY_ID=accesskey
AWS_SECRET_ACCESS_KEY=accesssecret

AUTH0_DOMAIN=yourdomain.auth0.com
AUTH0_CLIENT_ID=************
AUTH0_CLIENT_SECRET=**************
REDIRECT_URI=*************
POST_LOGOUT_REDIRECT_URI=*********
SESSION_COOKIE_SECRET=************ //40文字以上のランダム文字列
AUDIENCE=******  //これはのちにAPIの保護で使うもので、今回はあまり関係ありません

テストの段階では、localhost:3000で動かすため、REDIRECT_URIPOST_LOGOUT_REDIRECT_URI.envから削除しておいてください。

Preparing APIs

次に、ログインやログアウトを取り回すAPIをpages/apiに作っていきます。

terminal
mkdir pages
mkdir pages/api
touch pages/api/{login.js,logout.js,callback.js,me.js}

lib/auth0の作りを公式とは少し変えていますので、それにあわせて変更しています。
Auth0のライブラリが細かいところをマネージしてくれるので、ひとつひとつは非常にシンプルですね。

login.js
import auth0 from '../../lib/auth0';

export default async function login(req, res) {
  try {
    await auth0().handleLogin(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}
logout.js
import auth0 from '../../lib/auth0';

export default async function logout(req, res) {
  try {
    await auth0().handleLogout(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}
callback.js
import auth0 from '../../lib/auth0';

export default async function callback(req, res) {
  try {
    await auth0().handleCallback(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}
me.js
import auth0 from '../../lib/auth0';

export default async function me(req, res) {
  try {
    await auth0().handleProfile(req, res);
  } catch (error) {
    console.error(error);
    res.status(error.status || 500).end(error.message);
  }
}

callback.jsは、ログイン時のcallbackにあたります。
上の設定時に、Auth0で、

としたのは、このためです。
/api/loginを経て、Auth0の認証画面から戻ってきた際、callbackにはクエリストリングとしてsession情報(やstate。今回はsessionしかありません)が渡されてきます。
handleCallbackは、このクエリストリングの情報を取得し、検証後に暗号化されたsession cookieを保存する役割を果たしています。

Handling User State

componentsを作り始める前に、もうひと手間!
ユーザーのログイン状態を取り回すuser.jslibに作っておきます。
個人的には、contextsやhocsに切り分けて書かれているほうが読みやすいような気がしたんですが、それはまた別の機会に挑戦してみます。

terminal
touch lib/user.js
user.js
import React from 'react';
import fetch from 'isomorphic-unfetch';

// グローバルにユーザーを保存することで、ページ遷移時に再度APIを呼んで読み込むことを回避。
let userState;

const User = React.createContext({ user: null, loading: false });

export const fetchUser = async () => {
  if (userState !== undefined) {
    return userState;
  }

// 先ほど作った'api/me'を呼んで、ログインしているユーザーの情報を取得。
  const res = await fetch('/api/me');
  userState = res.ok ? await res.json() : null;
  return userState;
};

export const UserProvider = ({ value, children }) => {
  const { user } = value;

  // SSR時にユーザーがfetchされていれば、userStateに追加。これにより、再度fetchする必要がなくなる。
  React.useEffect(() => {
    if (!userState && user) {
      userState = user;
    }
  }, []);

  return <User.Provider value={value}>{children}</User.Provider>;
};

export const useUser = () => React.useContext(User);

export const useFetchUser = () => {
  const [data, setUser] = React.useState({
    user: userState || null,
    loading: userState === undefined
  });

  React.useEffect(() => {
    if (userState !== undefined) {
      return;
    }

    let isMounted = true;

    fetchUser().then((user) => {
      // componentがまだマウントされているときだけユーザーをセット。
      if (isMounted) {
        setUser({ user, loading: false });
      }
    });

    return () => {
      isMounted = false;
    };
  }, [userState]);

  return data;
};

Creating Components

いよいよComponentの準備です。
ここでは、ヘッダーにログイン/ログアウトのボタンがある、一般的なレイアウトのページを作っていきます。

terminal
mkdir components
touch components/{header.jsx,layout.jsx}
header.jsx
import React from 'react';
import Link from 'next/link';

import { useUser } from '../lib/user'; //先ほどのuserからユーザーの状態をもらってくる。

const Header = () => {
  const { user, loading } = useUser();

  return (
    <header>
      <nav>
        <ul>
          <li>
            <Link href="/">
              <a>Home</a>
            </Link>
          </li>
          {!loading &&
            (user ? (
              <>
                <li>
                  <Link href="/profile">
                    <a>Profile</a>
                  </Link>
                </li>{' '}
                <li>
                  <a href="/api/logout">Logout</a>
                </li>
              </>
            ) : (
              <>
                <li>
                  <a href="/api/login">Login</a>
                </li>
              </>
            ))}
        </ul>
      </nav>

      <style jsx>{`
        header {
          padding: 0.2rem;
          color: #fff;
          background-color: #333;
        }
        nav {
          max-width: 42rem;
          margin: 1.5rem auto;
        }
        ul {
          display: flex;
          list-style: none;
          margin-left: 0;
          padding-left: 0;
        }
        li {
          margin-right: 1rem;
        }
        li:nth-child(1) {
          margin-right: auto;
        }
        a {
          color: #fff;
          text-decoration: none;
        }
        button {
          font-size: 1rem;
          color: #fff;
          cursor: pointer;
          border: none;
          background: none;
        }
      `}</style>
    </header>
  );
};

export default Header;

些細な点ですが、<style jsx>:nth-child(1)という書き方をしているところがあります。
これは、公式ではページのバリエーションがもう少し多かった(本当は3だった)ためなので、:first-childでもよいと思います。

layout.jsx
import React from 'react';
import Head from 'next/head';

import Header from './header';
import { UserProvider } from '../lib/user';

const Layout = ({ user, loading = false, children }) => (
  <UserProvider value={{ user, loading }}>
    <Head>
      <title>Next.js with Auth0</title>
    </Head>

    <Header />

    <main>
      <div className="container">{children}</div>
    </main>

    <style jsx>{`
      .container {
        max-width: 42rem;
        margin: 1.5rem auto;
      }
    `}</style>
    <style jsx global>{`
      body {
        margin: 0;
        color: #333;
        font-family: -apple-system, 'Segoe UI';
      }
    `}</style>
  </UserProvider>
);

export default Layout;

Creating Pages

いよいよpagesを作っていきます。

terminal
touch pages/{index.jsx,profile.jsx}
index.jsx
import React from 'react';

import Layout from '../components/layout';
import { useFetchUser } from '../lib/user';

export default function Home() {
  const { user, loading } = useFetchUser();

  return (
    <Layout user={user} loading={loading}>
      <h1>Next.js and Auth0 Example</h1>

      {loading && <p>Loading login info...</p>}

      {!loading && !user && (
        <>
          <h4>Try it!</h4>
          <p>
            To test the login click in <i>Login</i>
          </p>
        </>
      )}

      {user && (
        <>
          <h4>Welcome!</h4>
          <p>You successfully logged in!</p>
        </>
      )}
    </Layout>
  );
}
profile.jsx
import React from 'react';

import Layout from '../components/layout';
import { useFetchUser } from '../lib/user';

export default function Profile() {
  const { user, loading } = useFetchUser();

  return (
    <Layout user={user} loading={loading}>
      <h1>Profile</h1>

      {loading && <p>Loading profile...</p>}

      {!loading && user && (
        <>
          <p>Profile:</p>
          <pre>{JSON.stringify(user, null, 2)}</pre>
        </>
      )}
    </Layout>
  );
}

Test run!

まずはローカルでテストです。npm run devhttp://localhost:3000を確認してみます。

スクリーンショット 2020-05-17 13.53.15.png

こんな画面が出てきます。Loginをclickしてみます。

スクリーンショット 2020-05-17 13.54.45.png

Auth0が提供しているログインのウィジェットlockが表示されます。
lockもいろいろカスタマイズができて便利なのですが、今回の記事では触れません。

ログインを進めてみます。

スクリーンショット 2020-05-17 13.57.29.png

狙い通り、メッセージが切り替わり、headerのメニューがProfileLogoutに切り替わりました。
ここで試しにcookieをチェックしてみると、

スクリーンショット 2020-05-17 13.59.22.png

a0:sessionという名前で、セッションが保存されています。
また、指定通りSame-SiteLax、さらに標準でHttp-Onlyになっていることがわかります。

これは前述したように、login後のcallbackで/api/callbackに帰ってきた際に、handleCallbackによってクエリストリングから保存しなおされたものです。
今回の記事では触れませんが、APIの保護に利用するauth tokenなどを取り回す場合は、同様にhandleCallbackがcookieに置き直してくれる情報を利用していきます。

では、お楽しみのProfileページを見てみましょう。

スクリーンショット 2020-05-17 14.02.46.png

ここではGoogleを利用してログインしたので、OIDCで取れてくる情報がユーザー情報として表示されています。
subはユーザーのユニークなidにあたるものですが、Auth0の場合は{connection name}|{random id}という形式になっています。
connectionとはユーザープール名のようなものですが、socail loginの場合は上記のようにgoogle-oauth2などとid providerの種類が入ってきます。

また、ここでは、emailが含まれていないことに気づかれた方もいらっしゃるでしょう。
利用場面によっては、当然、「ユーザーのメールアドレスがほしい」ということもあると思います。

その場合は、.envに記述したscopeの部分にemailを追加し、openid profile emailとすると、メールアドレスが取れるようになります。

Deploy!

さて、最後にdeployの方法です。これはとんでもなく簡単です(注:まだやらないでください)。

terminal
npx serverless

終了! と言いたいところですが、上記したように、このままdeployしてもAuth0のSettingが済んでいません。

Auth0のコンソールを開き、上のAuth0の設定についてのセクションで、http://localhost:3000http://localhost:3000/api/callbackとしていた部分に、serverless.ymlで指定したドメインについても追記する必要があります。
あらためて画像を貼ると、console-->Applications-->Settings-->Application URLsのセクションです。

スクリーンショット 2020-05-16 18.54.13.png

また「テストの段階では削除しておいてください」と注記しておいた.envにも、修正が必要です。
REDIRECT_URIPOST_LOGOUT_REDIRECT_URIに、それぞれdeploy後の正しいURLを記述する必要があります。
REDIRECT_URIのほうが、https://your.domain.com/api/callbackの形になります。

ここまで確認できたら、上述のnpx serverlessを実行してみてください。
deployの速度が早いことも体感できると思います。
この高速deployを可能にしている大きな理由のひとつが、Serverless Next.jsがCloudFormationを利用していないことです。

Behind the Curtain

今回、nextjs-auth0を使ってみようと思ったのは、次のブログを読んだからでした。

https://auth0.com/blog/ultimate-guide-nextjs-authentication-auth0/

このブログでは、Next.jsでAuthenticationを実装する代表的なシナリオについて、それぞれの具体的な方法や長所・短所がまとめられています。

中でも興味をひかれたのが、 "Next.js Serverless Deployment Model" というセクションでした。
ここに、上でたびたび触れた、callbackでの挙動に関連した説明がありますので、はしょりながらですが、ざっと訳してみようと思います。

......Next.jsがその輝きを見せるのは、すべてのページやAPI Routeが、それぞれZEIT NowやAWS Lambdaのようなserverless functionとして実装される、serverless deployment modelのもとで利用される場面だ。

このモデルでは、(Express.jsのような)本格的なweb frameworkは存在しない。その代わり、ランタイムは( (req, res)=>{} という形で)リクエストとレスポンスのオブジェクトをやりとりする関数を実行することになる。そして、このことが我々がExpress.jsのような伝統的なweb frameworkや、Passport.jsのようにユーザーの認証を取り回したり、express-sessionsのようにsessionを作ったりする、できあいのパッケージを利用できない大きな理由になっている。

(中略)

......nextjs-auth0を利用すると、ユーザーはAuthorization Code Grantを利用してサインインすることになる。ユーザーはまず、必要なすべての認証認可ロジック(サインアップ、サインイン、MFA、<social loginなどの>許可など)を取り回すAuth0にリダイレクトされ、そののち、(サービス側の)アプリケーションにクエリストリングにAuthorizationCodeを含んだ状態でリダイレクトされて帰ってくる。

サーバサイド(というより、serverless function)は、このコードをid_token、またオプションとしてaccess_tokenrefresh_tokenと交換する。id_tokenが検証されたのち、セッションが作られ、暗号化されたcookieとして保存される。ページが(サーバサイドで)レンダリングされるか、API Routeが呼ばれるたびに、session cookieがserverless functionsに渡されることで、serverless functionsはセッションや関連するユーザー情報にアクセスすることができるようになる。

実のところ、今回、実験してみた範囲は、クライアントサイドのみでのuserの取り回しでした。
なので、たとえばProfileページのURLをログインしていない状態で直に叩くと、次のような画面になります。

スクリーンショット 2020-05-17 15.39.56.png

もちろん、不用意にuserの情報が露出するといったことはないわけですが、実際のサービスを構築する場面を想定すると、ユーザー情報が抜けた「枠」だけのページであっても、非ログイン状態のユーザーがアクセスできるのは、いささか不都合です。

しかし、上の翻訳にあるように、nextjs-auth0を利用する方法なら、サーバサイドでユーザー情報を利用することもできるわけです。

次回は、サーバサイドでuserを見ることで、非ログインユーザーによるURLへの直アクセスでログインを求める仕掛けを作る方法を書いてみたいと思います。


  1. ここでいう「Serverless Next.js」は、Next.jsのserverless modeについてではなく、それを利用したserverless componentとして提供されているserverless-next.jsを指します。 

  2. 本書き込みは、基本的に初学者の「やってみた話」です。productionレベルでのご検討、より正確な情報は各種公式ドキュメント等をご参照ください。 

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

Reactで星のレーティングを実装する

はじめに

Reactを使っていて星のレーティングを実装する機会があったので備忘録として記します。
ちなみに今回はパッケージを一切使っていないため、パッケージを使ってもっとお洒落なレーティングを作りたいと思った方はググって見てください。

完成形

ezgif.com-optimize.gif
※これよりもお洒落なレーティングを実装したい方はパッケージなどのコンポーネントをググってみてください。

作り方

作り方はざっくり言うとこんな感じです。

  1. Stateで光らせる星の数を管理する
  2. 星それぞれをboolean(trueかfalse)でどれを光らせるかを決定する
  3. stateを参照しながらbooleanが入った配列を作る(星の数がMaxで5つなら作る配列の要素は5つ)
  4. 「3」で作った配列をループさせてJSX要素(今回は星)をはき出す。その時、配列のindex番号をはき出した要素に振り分ける
  5. onClickメソッドでハンドラー(関数)を当てて引数で振り分けたindex番号を受け取る
  6. ハンドラー(関数)でstateを更新

コードで見ていきましょう

import React, { useState } from 'react';

export const StarRating: React.FC<> = () => { 
  const [ratingState, setRatingState] = useState<number>(0);
  const stars: boolean[] = [];
  for (let i = 0; i > 5; i++) {
    if( i < ratingState ) {
      stars.push(true);
    } else {
      stars.push(false);
    }
  }
  const HandleStarShine = (rating) => {
    setRatingState( rating + 1 );  // index番号は0~4だから星の数に合わせて1~5にする
  };
  return (
    {stars.map((val: boolean, index: number) => {
      return (
        <button onClick={() => HandleStarShine(index)} >
          {val ? <img src="光る星マーク" /> : <img src="光ってない星マーク"/>
        </button>
    );
    })}
  );
};

こんな感じです。
コードにあるようにstateは押されたボタン(星)が持っているindex番号の値に更新されます。

まとめ

さて、この記事の星のレーティングはいくつでしたか?
また、ご指摘などもありましたらコメントの方をよろしくお願いします。
最近Reactを勉強しはじめたので、ハンドラーに引数を渡すのが個人的に難しかったのですが、今思えばReactのチュートリアルの三目並べゲームで引数を渡すのはすでにやってましたね(笑)勉強あるのみです(笑)

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

firebase auth + react hooks (context, reducer) で認証

概要

Firebase の認証機構を使って、Reactでサインイン周りを実装するサンプル(メモ)です。

実装イメージはこんな感じです。

図1.png

ファイル構造

色々ファイルを追加するので予めファイル構造を。

- src
    - components
        - AuthCheck.js
    - cnofig
        - Firebase.js
    - store
        - userStore.js
    - App.js
    - Dashboard.js
    - index.js
    - Signin.js
    - App.css

準備

create-react-appで雛形を用意しておいてください。
firebase、 react-router-dom、そのほかにloading中に他コンポーネントを触らせないようにする react-loading-overlayもインストールします。

# npm i -S firebase react-router-dom react-loading-overlay

firebase設定の詳細は割愛します。
configの下にFirebase.jsを作成し、利用するfirebaseの構成オブジェクトを以下のように保存しておいてください。

import firebase from 'firebase';

const firebaseConfig = {
  apiKey: "api-key",
  authDomain: "project-id.firebaseapp.com",
  databaseURL: "https://project-id.firebaseio.com",
  projectId: "project-id",
  storageBucket: "project-id.appspot.com",
  messagingSenderId: "sender-id",
  appID: "app-id",
};


firebase.initializeApp(firebaseConfig);

export default firebase;

ユーザ情報格納部分の実装

ユーザ情報は、propsの代わりにcontextというhookを使って保存するようにします。contextを使うことで子コンポーネントにpropsを引き継いでいく必要がないという利点があります。
ユーザ情報の変更は reducerというhookを使います。

この2つを使って userStore.js を実装します。

/src/store/userStore.js
import React, { createContext, useReducer } from "react";

const initialState = { user: null }

const store = createContext(initialState);

const { Provider } = store;

const StateProvider = ({ children }) => {
    const [state, dispatch] = useReducer((state, action) => {
        return { ...state, user: action.user }
    }, initialState);
    return <Provider value={{ state, dispatch }}>{children}</Provider>
}

export { store, StateProvider }

storeはコンテキストを入れています。また StateProviderはReact.Providerで、子コンポーネントにコンテキストの変更を提供するためのコンポーネントです。contextはinitialStateで初期化しています。

コンテキストを全体で共有できるようにする

次に作成したStateProviderをindex.jsに適応します。
全体でユーザ情報の変更を取得することができるようになります。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { StateProvider } from './store/userStore'

const app = (
        <StateProvider>
            <App />
        </StateProvider>
)
ReactDOM.render(app, document.getElementById('root'));

Signinとtopページ(サインイン後)ページを作る

先にサインインページを作ります。
handleSigninでfirebaseにメールアドレスとパスワードでサインインします。(firebaseのログインプロバイダ メール/パスワード を有効にしておく必要があります)
認証に成功した場合は、dispatchでユーザ情報を設定し、トップページに移動させ、失敗した場合はアラートを表示させるようにしています。
ユーザ情報としてサインインを実行した時に戻ってきたfirebase.Userを設定しています。

/Signin.js
import React, { useState, useContext } from "react";
import { store } from "./store/userStore";
import { useHistory } from "react-router-dom";
import firebase from "./config/Firebase";

const Signin = () => {
    const { state, dispatch } = useContext(store);
    const history = useHistory();
    const [formdata, setFormdata] = useState({
        email: "",
        password: ""
    });

    const handleSignin = async () => {
        try {
            const user = await firebase.auth().signInWithEmailAndPassword(formdata.email, formdata.password);

            if (user) {
                await dispatch({ user: user.user });
                history.push("/");
            } else {
                alert("エラー!");
            }

        } catch (e) {
            alert("認証失敗");
        }
    }

    const handleChange = (e) => {
        e.preventDefault();
        setFormdata({ ...formdata, [e.target.name]: e.target.value });
    }

    return (
        <div>
            <h2>サインインページ</h2>
            <div>
                email: <input type="text" id="email" name="email" onChange={handleChange} value={formdata.email} />
            </div>
            <div>
                password: <input type="password" id="password" name="password" onChange={handleChange} value={formdata.password} />
            </div>
            <div>
                <button onClick={handleSignin}>サインイン</button><br />
            </div>
        </div>
    )
}

export default Signin;

次に、トップページを実装します。
トップページでは、サインイン時に取得したユーザ情報(state.user.email)をcontextから取得して表示しています。

Dashboard.js
import React, { useContext } from "react";
import { store } from "./store/userStore";
import firebase from "./config/Firebase";
import { useHistory } from "react-router-dom"

const Dashboard = () => {
    const { state, dispatch } = useContext(store);
    const history = useHistory();

    const handleSignout = async () => {
        await firebase.auth().signOut();
        history.push('/signin');
    }

    return (
        <div>
            <h2>ダッシュボード</h2>
            {console.log(state.user)}
            <p>こんにちは {state.user ? state.user.email : null} さん</p>
            <hr />
            <button onClick={handleSignout}>サインアウト</button>
        </div>
    )
}

export default Dashboard;

認証チェックAuthCheckの実装

firebaseにすでにサインインしているかどうかをチェックし、している場合はユーザ情報をcontextに反映、していない場合はサインインページに飛ばすようにします。

/src/components/AuthCheck.js
import React, { useContext, useState, useEffect } from "react";
import { store } from '../store/userStore';
import { Redirect } from "react-router-dom";
import firebase from "../config/Firebase";
import LoadingOverlay from 'react-loading-overlay';

const AuthCheck = ({ children }) => {

    const { state, dispatch } = useContext(store);
    const [checked, setChecked] = useState(false);

    useEffect(() => {
        const check = async () => {
            firebase.auth().onAuthStateChanged(async user => {
                console.log(user);
                if (user) {
                    dispatch({ user: user });
                }
                setChecked(true);
            });
        }
        check();
    }, [])

    if (checked) {
        if (state.user) {
            return children;
        }
        else {
            return <Redirect to="/signin" />
        }
    } else {
        return (
            <LoadingOverlay
                active={true}
                spinner
                text='Loading...'
            >
                <div style={{ height: '100vh', width: '100vw' }}></div>
            </LoadingOverlay>
        );
    }
}

export default AuthCheck;

ルーティングの設定

最後にApp.jsに各ページのルーティングを設定します。

Dashboardはサインイン後に表示させるので、Authコンポーネントで囲みます。

App.js
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

import AuthCheck from './components/AuthCheck';
import Signin from './Signin';
import Dashboard from './Dashboard';

function App() {
    return (
        <div className="App">
            <Router>
                <Switch>
                    <Route path="/signin" name="サインイン" exact component={Signin} />
                    <AuthCheck>
                        <Route path="/" name="ダッシュボード" exact component={Dashboard} />
                    </AuthCheck>
                </Switch>
            </Router>
        </div>
    );
}

export default App;

これで終了です。
実行すると以下のようにサインインページが表示されます。
スクリーンショット 2020-05-17 12.13.27.png

サインイン後
スクリーンショット 2020-05-17 12.21.57.png

サインアウトするとサインインページに戻ります。

また、firebaseのサインイン情報はブラウザにキャッシュされているので、サインアウトせずにアプリを終了させ、再度アプリを起動するとサインインしたままなので、トップページ(Dashboard.js)に遷移することが確認できます。

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

React文法チートシート

この内容について

この内容は、私が運営しているサイトに、より見やすく掲載しているので、よければそちらもご活用ください。
Reactチートシート | コレワカ

Reactとは

ReactはUI構築のためのJavaScriptライブラリのこと
React公式サイト

基本的な書き方

HTML
<div id="app">
  <!-- React.js適用範囲 -->
</div>
JSX
class App extends React.Component{
  render(){
    return <div>Hello World</div>;
  }
}
const ROOT = document.querySelector('#app');
ReactDOM.render(<App/>, ROOT);

コンポーネント

基本コンポーネント

Reactコンポーネントを作成する


See the Pen
React_component
by engineerhikaru (@engineerhikaru)
on CodePen.


関数コンポーネント

関数表記により定義されたコンポーネント。状態制御やクラス構造が不要な場合に使用する


See the Pen
React_function
by engineerhikaru (@engineerhikaru)
on CodePen.


ネスト

定義したReactコンポーネントを入れ子にする


See the Pen
React_nest
by engineerhikaru (@engineerhikaru)
on CodePen.


イベント

onClick

clickイベントを設置する


See the Pen
React_onClick
by engineerhikaru (@engineerhikaru)
on CodePen.


onChange

changeイベントを設置する


See the Pen
React_onChange
by engineerhikaru (@engineerhikaru)
on CodePen.


ライフサイクル

constructor

Mounting時に一番最初に呼ばれるメソッド。主にstateの初期化に使用する


See the Pen
React_constructor
by engineerhikaru (@engineerhikaru)
on CodePen.


componentDidMount

ComponentがDOMにMountされた後に呼ばれるメソッド。主にAjaxの処理やsetIntervalなどのイベントに使用する


See the Pen
React_componentDidMount
by engineerhikaru (@engineerhikaru)
on CodePen.


shouldComponentUpdate

新しいprops,stateを受け取りレンダリングされる前に呼ばれるメソッド。主に不要な再レンダリングを抑制してパフォーマンスの低下を防ぐ目的で使用する


See the Pen
React_shouldComponentUpdate
by engineerhikaru (@engineerhikaru)
on CodePen.


componentWillUnmount

ComponentをUnmountされる時に呼ばれるメソッド。主にcomponentDidMountの処理の解除で使用する


See the Pen
React_componentWillUnmount
by engineerhikaru (@engineerhikaru)
on CodePen.


その他

JSの埋め込み

JSXの記述部分にJSを埋め込む


See the Pen
React_jsembed
by engineerhikaru (@engineerhikaru)
on CodePen.


コメント

See the Pen React_comment by engineerhikaru (@engineerhikaru) on CodePen.

この内容について

この内容は、私が運営しているサイトに、より見やすく掲載しているので、よければそちらもご活用ください。
Reactチートシート | コレワカ

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

React Native for macOS でサンプルアプリを動かす

React Native for macOS

MicrosoftがReact NativeをフォークしてパッチをあてたReact Native for Windowsに加えて、 React Native for macOSをGitHubで公開しました。

Get Started with macOSの手順でスケルトンアプリと、 rssreaderの2つを動かしてみました。

image.png

React Native for macOSでmacアプリを起動させる

すでにReact Nativeの開発環境が整っていれば、簡単にアプリを起動させることができます。

Get Started with macOS を読みながら進めます。

環境

Tools バージョン
macOS Catalina 10.15.4
Xcode 11.4.1
node 12.14.1
npx 6.14.2
yarn 1.21.1
pod 1.9.1
watchman 4.9.0

react-native init でプロジェクトを作成

今回は RNMacosSample というプロジェクトで進めます。

npx react-native init RNMacosSample --version 0.61.5

エラーなく終了し、最後に以下の表示が出れば成功です。RNMacosSample ディレクトリが作成されます。

% npx react-native init RNMacosSample --version 0.61.5
This will walk you through creating a new React Native project in /Users/shoken/git/sample
Using yarn v1.22.4
Installing react-native@0.61.5...
# ----省略----
✨  Done in 9.44s.
info Installing required CocoaPods dependencies

  Run instructions for iOS:
    • cd "/Users/shoken/git/RNMacosSample" && npx react-native run-ios
    - or -
    • Open RNMacosSample/ios/RNMacosSample.xcworkspace in Xcode or run "xed -b ios"
    • Hit the Run button

  Run instructions for Android:
    • Have an Android emulator running (quickest way to get started), or a device connected.
    • cd "/Users/shoken/git/RNMacosSample" && npx react-native run-android

macOS extension をインストール

プロジェクトディレクトリに移動して、 macOS用のライブラリをインストールします。同時にPodもインストールされます。

cd RNMacosSample
npx react-native-macos-init

エラーなく終了し、最後に以下の表示が出れば成功です。

% npx react-native-macos-init
npx: installed 114 in 6.575s
Reading application name from package.json...
Reading react-native version from node_modules...
Reading react-native version from node_modules...
# ----省略----
Generating Pods project
Integrating client project

[!] Please close any current Xcode sessions and use `RNMacosSample.xcworkspace` for this project from now on.
Pod installation complete! There are 29 dependencies from the Podfile and 26 total pods installed.

  Run instructions for macOS:
    • npx react-native run-macos
    - or -
    • Open macos/RNMacosSample.xcworkspace in Xcode or run "xed -b macos"
    • yarn start:macos
    • Hit the Run button

react-native run-macos でアプリを起動

npx react-native run-macos

React Nativeアプリと同様に、Terminalの別ウィンドウでMetro Bundlerが起動して、 RNMacosSampleアプリが起動したら成功です!

image.png

rssreaderアプリを起動させる

次に、リポジトリにPull Requestが送られているrssreaderを動かしてみます。

リポジトリをclone

git clone git@github.com:qmatteoq/react-native-windows-samples.git

rssreaderディレクトリに移動

cd react-native-windows-samples/samples/rssreader

rssreaderアプリを起動

yarn install
cd macos && pod install && cd ..
npx react-native run-macos

起動しました!

Screen Shot 2020-05-17 at 0.24.36.png

公開予定のReact Native for macOSの公式ブログ記事

リポジトリに5月更新のブログドラフト記事がPull Requestで送られていたので近日中に公開されそうです。

React Native for macOS, and more!

image.png

注意: 公式ドキュメント通りに進めるとエラー

2020年5月17日時点で、公式ドキュメントのGet Started with macOSにあるコマンドでは一部エラーになります。

一番最初の Install React Native for macOS に記載されている npx react-native init <projectName> --version 0.61 を実行するとエラーになります。

% npx react-native init sampleProject --version 0.61
This will walk you through creating a new React Native project in /Users/shoken/git/sample
Using yarn v1.22.4
Installing 0.61...
yarn add v1.22.4
info No lockfile found.
[1/4] ?  Resolving packages...
error An unexpected error occurred: "https://registry.yarnpkg.com/0.61: Not found".
info If you think this is a bug, please open a bug report with the information provided in "/Users/shoken/git/sample/yarn-error.log".
info Visit https://yarnpkg.com/en/docs/cli/add for documentation about this command.
Error: Command failed: yarn add 0.61 --exact
    at checkExecSyncError (child_process.js:621:11)
    at execSync (child_process.js:657:15)
    at run (/Users/shoken/.nodebrew/node/v12.14.1/lib/node_modules/react-native-cli/index.js:294:5)
    at createProject (/Users/shoken/.nodebrew/node/v12.14.1/lib/node_modules/react-native-cli/index.js:249:3)
    at init (/Users/shoken/.nodebrew/node/v12.14.1/lib/node_modules/react-native-cli/index.js:200:5)
    at Object.<anonymous> (/Users/shoken/.nodebrew/node/v12.14.1/lib/node_modules/react-native-cli/index.js:153:7)
    at Module._compile (internal/modules/cjs/loader.js:955:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:991:10)
    at Module.load (internal/modules/cjs/loader.js:811:32)
    at Function.Module._load (internal/modules/cjs/loader.js:723:14) {
  status: 1,
  signal: null,
  output: [ null, null, null ],
  pid: 1725,
  stdout: null,
  stderr: null
}
Command `yarn add 0.61 --exact` failed.

バージョン指定にパッチバージョンまで指定すると成功します。

- npx react-native init <projectName> --version 0.61
+ npx react-native init <projectName> --version 0.61.5

上記修正はPull Request 出したので、マージ待ちです。

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

Next.js × TypeScriptの同期・非同期処理をHooksを使って書く ~非同期処理編~

概要

こんにちは、よしデブです。

前回Next.js × TypeScriptの同期・非同期処理をHooksを使って書く ~同期的処理編~の続きです。

  1. Reduxを始めるの準備
  2. 同期処理でTodo追加・完了機能を作る
  3. 非同期処理でログイン機能を作る(今日はここ)
  4. (おまけ)その他のライブラリ紹介

やっと本シリーズのメインが書けます...!!
本記事では、Next.js × TypeScriptにおいて、非同期処理をRedux Hooksを使って書く方法を紹介します。
再三言ってきてますが、これから紹介する方法は私の思うベストプラクティス なので、もっと良い書き方があるよ!という方はコメントお待ちしておりますm(_ _)m

ログイン機能に関する状態管理を定義する

まず、ログイン機能に関するActionType、ActionCreator、Reducerを定義していきます。
私は非同期処理については原則
1. リクエスト開始状態
2. リクエスト成功状態
3. リクエスト失敗状態

の3状態を持つようにしています。リクエスト開始状態でローディング処理をし、リクエスト成功状態で成功結果を表示、リクエスト失敗状態で失敗結果を表示といった具合に切り分けています。

src/store/auth/types.ts
export default {
  FETCH_LOGIN: 'FETCH_LOGIN',
  FETCH_LOGIN_SUCCESS: 'FETCH_LOGIN_SUCCESS',
  FETCH_LOGIN_FAILURE: 'FETCH_LOGIN_FAILURE'
} as const
src/store/auth/actions.ts
import { User } from '@store/auth/index'
import types from './types'

export function requestLogin() {
  return {
    type: types.FETCH_LOGIN,
  }
}

export function successLogin(user: User) {
  return {
    type: types.FETCH_LOGIN_SUCCESS,
    payload: {
      user
    }
  }
}

export function failureLogin() {
  return {
    type: types.FETCH_LOGIN_FAILURE
  }
}
src/store/auth/index.ts
import { Actions } from '../actions'
import types from './types'

export interface User {
  name: string
}

interface State {
  isFetching: boolean,
  user?: User
}

export function initialState(injects?: State): State {
  return {
    user: undefined,
    isFetching: false,
    ...injects,
  }
}

export function reducer(state = initialState(), action: Actions): State {
  switch (action.type) {
    // リクエストスタート 通信中の状態にする(isFetching=true)
    case types.FETCH_LOGIN:
      return { ...state, isFetching: true }
    // リクエスト成功 通信終了(isFetching=false)にし、取得したユーザ情報を保存する
    case types.FETCH_LOGIN_SUCCESS:
      return { ...state, isFetching: false, user: action.payload.user }
    // リクエスト失敗 通信終了(isFetching=false)にすること以外今回は何もしない
    case types.FETCH_LOGIN_FAILURE:
      return { ...state, isFetching: false }
    default:
      return state
  }
}

Actions型、RootReducer、RootStateの変更

authという新たな状態が作成されたのでsrc/store/actions.tsを以下のように変更します。
このように状態が増えるたびに CreatorsToActionsを追加するだけで簡単に型推論ができちゃいます。

src/store/actions.ts
type Unbox<T> = T extends { [K in keyof T]: infer U } ? U : never
type ReturnTypes<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any
  ? ReturnType<T[K]>
  : never
}
type CreatorsToActions<T> = Unbox<ReturnTypes<T>>

export type Actions = CreatorsToActions<typeof import('./todos/actions')>
                    | CreatorsToActions<typeof import('./auth/actions')> // 追加

/** Actionsの推論結果
type Actions = {
  type: 'ADD_TODO'
  payload: {
    id: string,
    done: boolean,
    task: string,
  }
} | {
  type: 'DONE_TODO'
  payload: {
    id: string
  }
} | {
  type: 'FETCH_LOGIN'
} | {
  type: 'FETCH_LOGIN_SUCCESS'
  payload: {
    user: User
  }
} | {
  type: 'FETCH_LOGIN_FAILURE'
}
*/

非同期処理用のActionCreatorを作る

redux-thunkを使った非同期処理用のActionCreatorは以下のような書き方を行います。
ポイントはリクエスト開始を知らせるdispatchを呼び出した後に、リクエスト成功または失敗をdispatchするようなPromiseを返す関数」を返すようにします。
ややこしいですが、非同期用のActionCreatorは通常のActionCreatorと違って関数を返しています。

apiは、axoisのインスタンスです。src/common/api.ts でbaseURLなどのAPIを叩く際に必要な共通の設定をしています。
今回は紹介しませんが、 JWT認証等をする時は私はここでheaderにトークンを入れるようにしてます。

src/store/auth/asyncActions.ts
import { LoginFormValues } from '@components/organisms/LoginForm/LoginForm'
import { Action, Dispatch } from 'redux'
import { failureLogin, requestLogin, successLogin } from '@store/auth/actions'
import api from '@common/api'

export function login(values: LoginFormValues) {
  return async (dispatch: Dispatch<Action>) => {
    // リクエストスタート(リクエスト開始状態にする)
    dispatch(requestLogin());
    return api({
      method: "post",
      url: '/api/login',
      data: {
        'login_id': values.login_id,
        'password': values.password
      }
    }).then((response) => {
      // リクエスト成功(アクセストークンをローカルに保存)
      localStorage.setItem('jwt', response.data.access_token)
      // リクエスト成功状態にして、ユーザ情報を渡す
      dispatch(successLogin(response.data.user))
    }).catch((response) => {
      // リクエスト失敗
      dispatch(failureLogin())
    })
  };
}
src/common/api.ts
import axios, {Method} from 'axios'

const api = axios.create({
  baseURL: process.env.API_URL,
  xsrfHeaderName: 'X-CSRF-Token',
  withCredentials: true,
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
  responseType: 'json'
})

export default api;

ログインフォームを作る

前回作成したTodo入力フォームと同様にユーザIDとパスワードを入力するためのログインフォームを作成します。
前回と違う点はログインボタンを押した時に非同期処理用のActionCreator(login)をdispatchしている事だけです。
しかもその書き方は 普通のActionCreatorをdispatchする方法と同じなので、直感的に理解しやすいかと思います。

また、通信状態を表すisFetchingを使って、通信中ならローディングを表示することも簡単に実現することができます。

src/components/organisms/LoginForm/LoginForm.tsx
import ErrorText from '@components/atoms/forms/ErrorText'
import TextField from '@components/molecules/TextField/TextField'
import Button from '@material-ui/core/Button'
import CircleProgress from '@material-ui/core/CircularProgress'
import {login} from '@store/auth/asyncActions'
import { StoreState } from '@store/index'
import {
  Field,
  FieldProps,
  Form,
  Formik,
} from 'formik'
import React, {FC} from 'react'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import * as Yup from 'yup';


const FieldWrapper = styled.div`
  margin-top: 30px;
  margin-left: 40px;
`

const LoginButtonWrapper = styled.div`
  margin-top: 40px;
  margin-left: 40px;
`

const LoginSchema = Yup.object().shape({
  login_id: Yup.string()
    .required('入力してください'),
  password: Yup.string()
    .required('入力してください'),
});

export interface LoginFormValues {
  login_id: string;
  password: string;
}

const LoginForm: FC = ({}) => {
  const auth = useSelector((state: StoreState) => state.auth);

  const dispatch = useDispatch();
  const initialValues: LoginFormValues = { login_id: '', password: '' };

  // ログインボタンが押されたらlogin ActionCreatorをdispatchする
  const handleSubmit = (values: LoginFormValues) => {
    dispatch(login(values));
  }

  return(
    <Formik
      initialValues={initialValues}
      validationSchema={LoginSchema}
      onSubmit={handleSubmit}
      render={({errors, touched}) => (
        <Form>
          <Field
            name="login_id"
            render={(props: FieldProps) => {
              return (
                <FieldWrapper>
                  <TextField
                    label={'ID'}
                    type={'text'}
                    fieldProps={props}
                  />
                  {errors.login_id && touched.login_id &&
                  <ErrorText>
                    {errors.login_id}
                  </ErrorText>}
                </FieldWrapper>
              )
            }}
          />
          <Field
            name="password"
            render={(props: FieldProps) => {
              return (
                <FieldWrapper>
                  <TextField
                    label={'パスワード'}
                    type={'password'}
                    fieldProps={props}
                  />
                  {errors.password && touched.password &&
                  <ErrorText>
                    {errors.password}
                  </ErrorText>}
                </FieldWrapper>
              )
            }}
          />
          <LoginButtonWrapper>
            <Button type="submit" variant="contained" color="primary" disabled={auth.isFetching}>ログイン</Button>
            // 通信中ならローディングを表示する
            {auth.isFetching && <CircleProgress/>}
          </LoginButtonWrapper>
        </Form>
      )}
    />)
};

export default LoginForm;

ページを編集する

最後に、ログインフォームとユーザ情報を表示するようにページを編集します。
また、useSelector を用いてユーザ情報も取得するように編集して完成です。

pages/index.tsx
import LoginForm from '@components/organisms/LoginForm/LoginForm'
import TodoForm from '@components/organisms/TodoForm/TodoForm'
import { Button } from '@material-ui/core'
import Container from '@material-ui/core/Container'
import { StoreState } from '@store/index'
import { doneTodo } from '@store/todos/actions'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'

/**
 * TopPage
 */
const TopPage = () => {
  const dispatch = useDispatch()
  // ユーザ情報も取得するように編集
  const [todos, user] = useSelector((state: StoreState) => [
    state.todos.todos,
    state.auth.user
  ])

  return (
    <main>
      <Container maxWidth="xs">
      <h1>Hello, World</h1>
      <h2>Todos</h2>
      <ul>
        {todos.map((todo, idx) => (
          <li key={idx}>
            <span
              style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
            >
              {todo.task}
            </span>
            <Button
              variant="contained"
              color="primary"
              disabled={todo.done}
              onClick={() => dispatch(doneTodo(todo.id))}
              style={{ marginLeft: 10 }}
            >
              DONE
            </Button>
          </li>
        ))}
      </ul>
      <TodoForm />
      // ここから追加
      <h2>Login</h2>
      <div>
        // ユーザ情報がある場合(ログインした場合)、ユーザ名を表示する
        {user ? 'こんにちは!' + user.name + 'さん' : 'ログインしてください'}
      </div>
      <LoginForm />
      // ここまで追加
      </Container>
    </main>
  )
}

export default TopPage

完成!!

demo3.gif

終わりに

Next.js × TypeScriptにおけるReduxの非同期処理をHooksで書く方法を紹介しました。
Hooksの登場で可読性がとても向上したように思います。また、TypeScriptによって型を意識したコーディングが可能になって、補完が効くようになって大変便利になりました。

本記事がよかったらLGTMお願いします。ありがとうございました!!
次回は本シリーズで紹介できなかったライブラリをおまけで紹介できたらと思います。

前回の記事たち

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