- 投稿日:2020-05-17T23:48:28+09:00
React(gatsby.js)でhighlight.jsを使う
実装
highlight.jsを追加する。今回はES6以上で書きます。
さらに初歩的なところを知りたい方はこちらのブログを読んでください。// npmモジュール追加 yarn add highlight.js今回はコードハイライトを使用する機会が記事のページでしか使わないため、Util的なものは実装せずに直接該当のReact Componentに突っ込む。
useEffect
がわからない方はこちらを確認してください。Article/index.jsximport 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のインスタンスが初期化以降は処理を走らせないようにフラグを用いてテキストのパース処理をスキップしているためです。
今回は初回以降もパースしてもらわないといけない要件なのでフラグを折りに行きます。Article/index.jsximport 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 />; }こんな感じに実装すると初回以降もハイライトしてくれるようになります。
アウトローな実装になりましたが、今回はこんなところで問題なく動いているので開発を終了しました。
- 投稿日:2020-05-17T22:44:40+09:00
RailsとReactをGraphQLでつなぐ
前回の記事でdocker-composeを使ってRailsとReactを立ち上げることができたので今回はこれらをGraphQLでつなぐところまでやっていきます!
Rails側のGraphQL設定
まずはRailsをGraphQLにつなぎます
※Rails側の設定についてはこちらの記事を参考にさせていただきました最初にGraphQLのgemをインストールします
Gemfilegem 'graphql' group :development do gem 'graphiql-rails' endこれで一回
bundle install
GraphQLが入ったので
rails g graphql:install
で必要なファイルが一通り作られますAPIモードでは一部追加されない部分があるので追加します
config/route.rbif Rails.env.development? mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' endconfig/application.rbrequire "sprockets/railtie"app/assets/config/manifest.js//= link graphiql/rails/application.css //= link graphiql/rails/application.js最後にReact側からの接続を有効にするためにapplication.rbに以下を追記します。こちらに関してはまだ理解が足りてないのでまた改めて記事を...!
config/application.rbconfig.hosts = nil config.autoloader = :classicこれでRails側の設定は完了です!
docker-compose up -d
をして、http://localhost:3000/graphiql
にアクセスするとgraphiqlの画面がでてきます左側に
query{ testField }と書いて▶ボタンを押してみましょう
右側に
{ "data": { "testField": "Hello World!" } }と出てきたら成功です!
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.jsimport 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.jsimport 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
にアクセスしてみましょう。下の画面のようにでてくれば成功です!以上でGraphQLでRailsとReactをつなぐ方法は終わりです!
まだまだ細かいところで理解不足なところが多いのでそこは引き続き勉強して拡充していければと思います
- 投稿日:2020-05-17T18:47:50+09:00
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.tsimport { 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.tsimport { 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をつなぐようになっていて、バンドルサイズ小さくなるよう工夫されてるなと思った
- 投稿日:2020-05-17T16:08:33+09:00
[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-devpackage.jsonにhuskyの設定を記述
package.json{ ... "husky": { "hooks": { "pre-commit": "npm run format:write" } } }Nxコマンドをインストール(任意)
Reactコンポーネントを@angular/cliっぽく自動生成したいのでnxコマンドをインストール。
※グローバルインストールを避けたい場合は毎回npx nx
とすれば良い。$ npm install -g @nrwl/cliMapboxをインストール
MapboxGLのReactコンポーネントを提供しているreact-map-glを利用する。
※TypeScriptで書くので型定義ファイルのインストールが必要$ npm install --save react-map-gl @types/react-map-glDeckGLをインストール
本題のdeck.glをインストールします。型定義ファイルは
@type
ではなく@danmarshall/deckgl-typingsから提供されている。$ npm install deck.gl --save $ npm install @danmarshall/deckgl-typings --save動作確認
ローカルサーバーを起動し、表示の確認を行う。
※Nxの初期画面が表示されればOK$ npm startReactコンポーネントを作成
@nx/cli
を使ってReactコンポーネントを自動生成する
@angular/cli
のgenerate
コマンドのように、コンポーネント、スタイルシート、テストコードの雛形がそれぞれ生成される$ nx g @nrwl/react:component my-map --project=deck-gl-app次回は生成したコンポーネントにMapboxの地図とdeck.glを利用したレイヤーをいくつか実装する。
- 投稿日:2020-05-17T15:49:32+09:00
初心者でもServerless Next.js+Auth0でログイン機能をもったserverlessなサイトが簡単に作れた話(1)
『何でも見てやろう』(小田実)の精神で、自分でもコードを書いてみる編集者・長尾です。
KODANSHAtech LLC.でゼネラルマネージャーやってます。
エンジニアのみなさん大募集中なので、メディアやコンテンツ開発に興味のある方はぜひ〜。
実はAuth0 Ambassadorもやってます。さて、今回はとっても便利なServerless Next.js1に、とっても便利なIdaaS、Auth0を組み合わせて、簡単便利にサーバレスな会員制サイトを作れそうということで、実験してみた一連の流れをご紹介してみます2!
少し書いてみたら結構ボリューミーだったので、以下のように3回に分けようと思います。
- セッティングとユーザーのログイン状態を取り回せる基本的なページの構築
- SSR時にユーザー情報を必要とすることで、そもそも非ログインユーザーには表示できないページを作る方法
- 外部のAPIにauth tokenを送って検証させる方法
元ネタは以下になります。
とくにauth0/nextjs-auth0
のexampleをなぞる部分が多くなっています。
- https://github.com/auth0/nextjs-auth0
- https://github.com/danielcondemarin/serverless-next.jsやってみた記事なので、変なところがあったら、ぜひご指摘ください…。
前提
serverless/Auth0など利用するリソースはこちら(折りたたんでいます)
- node
- npm
- serverless
このあたりまでは、すでに利用されている方向けです。
Go Serverless!も使っていきます。
そしてもちろん、IdaaSとしては、我らがAuth0です。serverlessでもいろいろなクラウドサービス(SaaS)が利用できますが、本記事ではAWSを利用する形で考えていきます。
Serverless Next.js
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."
いわゆるログイン機能、あるいは認証認可の機能を提供するサービスといえば、
Cognito
やFirebase Authentication
など、さまざまな選択肢があります。そんな中、認証認可基盤であることに完全に特化することで、APIを通じた「疎」な世界観にマッチしたIdaaSとして注目されているのが、Auth0です。
どんなサービスにも組み込みやすく、ソーシャルログインやOIDC、SAMLへの対応も簡単、しかもドキュメンテーションが非常に充実していて、何をどう取り回せばセキュアなID管理ができるのか、すぐに調べられるのも大きな特徴でしょう。
この記事の末尾で触れたいと思いますが、Next.jsをserverlessで利用する場面では、やはりAuth0が「疎」なIdaaSを志向していることが、強みを発揮すると思います。
いろいろ書きたくなるのですが、「やってみた」パートに早く移るため、詳細は下記のリンクを示すことで代えさせていただきます!
- (少し古いけど基本がよくわかる)認証プラットフォーム Auth0 とは?
- Classmethod諏訪さんのところのDevelopers.IOにも記事が多数
Getting Started
今回は、Serverless Next.jsにAuth0 Next.jsの公式exampleを組み合わせて、ログイン機能を実現し、ユーザー情報を表示させるところまでを試してみます。
まずは、Serverless Next.jsの準備からです。
Preparing serverless-next.js
terminalmkdir 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.ymlmyNextApplication: component: serverless-next.js実際の利用の場面では、サービスを独自のドメインで公開することになると思います。
その場合、Route53
を利用して設定するドメインの指定をここに記述します。serverless.ymlmyNextApplication: component: serverless-next.js inputs: domain: "example.com"サブドメインを利用する場合は、次のようになります。
serverless.ymlmyNextApplication: 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を入れておきます。適宜、ご利用のものに入れ替えてください。.envAWS_ACCESS_KEY_ID=accesskey AWS_SECRET_ACCESS_KEY=accesssecretこれはserverless-next.jsのために必要だという設定ではありませんが、あとで利用するために、
package.json
のscripts
は以下のようにしておきます。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!
+CREATE APPLICATION
で、Regular Web Applications
を選択し、新しいApplicationを作ります。作ったApplicationの
Settings
タブで、以下の情報が確認できるので、これをメモしておきます。
- Domain
- Client ID
- Client Secret
また、
Settings
の最下部に、Show Advanced Settings
がありますので、ここを開いて、OAuth
のタブを確認しておきましょう。
JsonWebToken Signiture Algorithm
はRS256
、OIDC Conformant
はonになっている必要があります。次に、ローカルでNext.jsのアプリを立ち上げて試す場合に、Auth0が機能するように、
Application URLs
のセクションに、次のように記入しておきます。
- Allowed Callback URLs: http://localhost:3000/api/callback
- Allowed Logout URLs: http://localhost:3000/
Preparing Next.js Application with Auth0
次に、Next.jsとAuth0でアプリケーションを作っていきます。
terminalnpm install --save next react react-dom touch next.config.js
next.config.js
には、以下の記述を入れます。next.config.jsmodule.exports = { target: "serverless" };今回は、Next.jsで簡単にAuth0が使える
auth0/nextjs-auth0
を利用していきます。https://github.com/auth0/nextjs-auth0
terminalnpm 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.jsimport { 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.jsif (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.js
のenv
で情報を読み込みます。next.config.jsconst 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のコンソールをみながらメモした情報です。.envAWS_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_URI
とPOST_LOGOUT_REDIRECT_URI
は.env
から削除しておいてください。Preparing APIs
次に、ログインやログアウトを取り回すAPIを
pages/api
に作っていきます。terminalmkdir pages mkdir pages/api touch pages/api/{login.js,logout.js,callback.js,me.js}
lib/auth0
の作りを公式とは少し変えていますので、それにあわせて変更しています。
Auth0のライブラリが細かいところをマネージしてくれるので、ひとつひとつは非常にシンプルですね。login.jsimport 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.jsimport 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.jsimport 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.jsimport 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で、
- Allowed Callback URLs: http://localhost:3000/api/callback
としたのは、このためです。
/api/login
を経て、Auth0の認証画面から戻ってきた際、callbackにはクエリストリングとしてsession情報(やstate。今回はsessionしかありません)が渡されてきます。
handleCallback
は、このクエリストリングの情報を取得し、検証後に暗号化されたsession cookie
を保存する役割を果たしています。Handling User State
componentsを作り始める前に、もうひと手間!
ユーザーのログイン状態を取り回すuser.js
をlib
に作っておきます。
個人的には、contextsやhocsに切り分けて書かれているほうが読みやすいような気がしたんですが、それはまた別の機会に挑戦してみます。terminaltouch lib/user.js
user.jsimport 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の準備です。
ここでは、ヘッダーにログイン/ログアウトのボタンがある、一般的なレイアウトのページを作っていきます。terminalmkdir components touch components/{header.jsx,layout.jsx}
header.jsximport 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.jsximport 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
を作っていきます。terminaltouch pages/{index.jsx,profile.jsx}
index.jsximport 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.jsximport 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 dev
→http://localhost:3000
を確認してみます。こんな画面が出てきます。
Login
をclickしてみます。Auth0が提供しているログインのウィジェット
lock
が表示されます。
lock
もいろいろカスタマイズができて便利なのですが、今回の記事では触れません。ログインを進めてみます。
狙い通り、メッセージが切り替わり、
header
のメニューがProfile
とLogout
に切り替わりました。
ここで試しにcookie
をチェックしてみると、
a0:session
という名前で、セッションが保存されています。
また、指定通りSame-Site
はLax
、さらに標準でHttp-Only
になっていることがわかります。これは前述したように、login後のcallbackで
/api/callback
に帰ってきた際に、handleCallback
によってクエリストリングから保存しなおされたものです。
今回の記事では触れませんが、APIの保護に利用するauth token
などを取り回す場合は、同様にhandleCallback
がcookieに置き直してくれる情報を利用していきます。では、お楽しみの
Profile
ページを見てみましょう。ここではGoogleを利用してログインしたので、OIDCで取れてくる情報がユーザー情報として表示されています。
sub
はユーザーのユニークなidにあたるものですが、Auth0の場合は{connection name}|{random id}
という形式になっています。
connection
とはユーザープール名のようなものですが、socail loginの場合は上記のようにgoogle-oauth2
などとid providerの種類が入ってきます。また、ここでは、
利用場面によっては、当然、「ユーザーのメールアドレスがほしい」ということもあると思います。その場合は、
.env
に記述したscope
の部分にopenid profile email
とすると、メールアドレスが取れるようになります。Deploy!
さて、最後にdeployの方法です。これはとんでもなく簡単です(注:まだやらないでください)。
terminalnpx serverless
終了! と言いたいところですが、上記したように、このままdeployしてもAuth0のSettingが済んでいません。
Auth0のコンソールを開き、上のAuth0の設定についてのセクションで、
http://localhost:3000
やhttp://localhost:3000/api/callback
としていた部分に、serverless.yml
で指定したドメインについても追記する必要があります。
あらためて画像を貼ると、console
-->Applications
-->Settings
-->Application URLs
のセクションです。また「テストの段階では削除しておいてください」と注記しておいた
.env
にも、修正が必要です。
REDIRECT_URI
とPOST_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_token
やrefresh_token
と交換する。id_token
が検証されたのち、セッションが作られ、暗号化されたcookieとして保存される。ページが(サーバサイドで)レンダリングされるか、API Routeが呼ばれるたびに、session cookieがserverless functionsに渡されることで、serverless functionsはセッションや関連するユーザー情報にアクセスすることができるようになる。実のところ、今回、実験してみた範囲は、クライアントサイドのみでの
user
の取り回しでした。
なので、たとえばProfile
ページのURLをログインしていない状態で直に叩くと、次のような画面になります。もちろん、不用意に
user
の情報が露出するといったことはないわけですが、実際のサービスを構築する場面を想定すると、ユーザー情報が抜けた「枠」だけのページであっても、非ログイン状態のユーザーがアクセスできるのは、いささか不都合です。しかし、上の翻訳にあるように、
nextjs-auth0
を利用する方法なら、サーバサイドでユーザー情報を利用することもできるわけです。次回は、サーバサイドで
user
を見ることで、非ログインユーザーによるURLへの直アクセスでログインを求める仕掛けを作る方法を書いてみたいと思います。
- 投稿日:2020-05-17T15:43:00+09:00
Reactで星のレーティングを実装する
はじめに
Reactを使っていて星のレーティングを実装する機会があったので備忘録として記します。
ちなみに今回はパッケージを一切使っていないため、パッケージを使ってもっとお洒落なレーティングを作りたいと思った方はググって見てください。完成形
※これよりもお洒落なレーティングを実装したい方はパッケージなどのコンポーネントをググってみてください。作り方
作り方はざっくり言うとこんな感じです。
- Stateで光らせる星の数を管理する
- 星それぞれをboolean(trueかfalse)でどれを光らせるかを決定する
- stateを参照しながらbooleanが入った配列を作る(星の数がMaxで5つなら作る配列の要素は5つ)
- 「3」で作った配列をループさせてJSX要素(今回は星)をはき出す。その時、配列のindex番号をはき出した要素に振り分ける
- onClickメソッドでハンドラー(関数)を当てて引数で振り分けたindex番号を受け取る
- ハンドラー(関数)で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のチュートリアルの三目並べゲームで引数を渡すのはすでにやってましたね(笑)勉強あるのみです(笑)
- 投稿日:2020-05-17T12:25:51+09:00
firebase auth + react hooks (context, reducer) で認証
概要
Firebase の認証機構を使って、Reactでサインイン周りを実装するサンプル(メモ)です。
実装イメージはこんな感じです。
ファイル構造
色々ファイルを追加するので予めファイル構造を。
- 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-overlayfirebase設定の詳細は割愛します。
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.jsimport 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.jsimport 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.jsimport 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.jsimport 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.jsimport 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.jsimport 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;これで終了です。
実行すると以下のようにサインインページが表示されます。
サインアウトするとサインインページに戻ります。
また、firebaseのサインイン情報はブラウザにキャッシュされているので、サインアウトせずにアプリを終了させ、再度アプリを起動するとサインインしたままなので、トップページ(Dashboard.js)に遷移することが確認できます。
- 投稿日:2020-05-17T09:17:00+09:00
React文法チートシート
この内容について
この内容は、私が運営しているサイトに、より見やすく掲載しているので、よければそちらもご活用ください。
Reactチートシート | コレワカReactとは
ReactはUI構築のためのJavaScriptライブラリのこと
React公式サイト基本的な書き方
HTML<div id="app"> <!-- React.js適用範囲 --> </div>JSXclass 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チートシート | コレワカ
- 投稿日:2020-05-17T01:13:17+09:00
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つを動かしてみました。
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-androidmacOS 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 buttonreact-native run-macos でアプリを起動
npx react-native run-macosReact Nativeアプリと同様に、Terminalの別ウィンドウでMetro Bundlerが起動して、
RNMacosSample
アプリが起動したら成功です!rssreaderアプリを起動させる
次に、リポジトリにPull Requestが送られているrssreaderを動かしてみます。
リポジトリをclone
git clone git@github.com:qmatteoq/react-native-windows-samples.gitrssreaderディレクトリに移動
cd react-native-windows-samples/samples/rssreader
rssreaderアプリを起動
yarn install cd macos && pod install && cd .. npx react-native run-macos起動しました!
公開予定のReact Native for macOSの公式ブログ記事
リポジトリに5月更新のブログドラフト記事がPull Requestで送られていたので近日中に公開されそうです。
React Native for macOS, and more!
注意: 公式ドキュメント通りに進めるとエラー
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 出したので、マージ待ちです。
- 投稿日:2020-05-17T00:55:25+09:00
Next.js × TypeScriptの同期・非同期処理をHooksを使って書く ~非同期処理編~
概要
こんにちは、よしデブです。
前回Next.js × TypeScriptの同期・非同期処理をHooksを使って書く ~同期的処理編~の続きです。
- Reduxを始めるの準備
- 同期処理でTodo追加・完了機能を作る
- 非同期処理でログイン機能を作る(今日はここ)
- (おまけ)その他のライブラリ紹介
やっと本シリーズのメインが書けます...!!
本記事では、Next.js × TypeScriptにおいて、非同期処理をRedux Hooksを使って書く方法を紹介します。
再三言ってきてますが、これから紹介する方法は私の思うベストプラクティス なので、もっと良い書き方があるよ!という方はコメントお待ちしておりますm(_ _)mログイン機能に関する状態管理を定義する
まず、ログイン機能に関するActionType、ActionCreator、Reducerを定義していきます。
私は非同期処理については原則
1. リクエスト開始状態
2. リクエスト成功状態
3. リクエスト失敗状態の3状態を持つようにしています。リクエスト開始状態でローディング処理をし、リクエスト成功状態で成功結果を表示、リクエスト失敗状態で失敗結果を表示といった具合に切り分けています。
src/store/auth/types.tsexport default { FETCH_LOGIN: 'FETCH_LOGIN', FETCH_LOGIN_SUCCESS: 'FETCH_LOGIN_SUCCESS', FETCH_LOGIN_FAILURE: 'FETCH_LOGIN_FAILURE' } as constsrc/store/auth/actions.tsimport { 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.tsimport { 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.tstype 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.tsimport { 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.tsimport 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.tsximport 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.tsximport 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完成!!
終わりに
Next.js × TypeScriptにおけるReduxの非同期処理をHooksで書く方法を紹介しました。
Hooksの登場で可読性がとても向上したように思います。また、TypeScriptによって型を意識したコーディングが可能になって、補完が効くようになって大変便利になりました。本記事がよかったらLGTMお願いします。ありがとうございました!!
次回は本シリーズで紹介できなかったライブラリをおまけで紹介できたらと思います。前回の記事たち