- 投稿日:2020-07-27T23:26:38+09:00
Next.jsでコンポーネントの使い方を学んだ
コード
実際に簡単なコンポーネントを作成してみた
Component.tsximport React from 'react'; export enum FontSize { Small, Large, } type props = { image: string; alt: string; size: FontSize; }; export const Component: React.FC<props> = ({ image, alt, size }: props) => { let text; let message; let font; switch (size) { case FontSize.Small: font='font-hairline'; message = 'This is fine print'; break; case FontSize.Large: font='font-black'; text = 'This is BOLD print'; break; } return ( <div className="w-full my-12 mx-auto text-center"> <div className="w-1/2 m-auto"> <img src={image} alt={alt} /> </div> <div className={`${font}`}>{message}</div> <div className={`mb-1 ${font}`}>{text}</div> </div> ); };Component.stories.tsximport { Component, FontSize } from '~/components/molecules/review/countZero'; import React from 'react'; export default { title: 'molecules/review/component', component: Component, }; export const SmallLetter = () => { return ( <Component image={ 'https://1.bp.blogspot.com/-fF4CgmrOpJs/XnLn8I0oy0I/AAAAAAABX0w/nBUniFZ1Q68Qf5wSY6jbHRb66N8GqaPmQCNcBGAsYHQ/s1600/figure_gray_zone.png' } alt={'細字'} size={FontSize.Small} /> ); }; export const LargeLetter = () => { return ( <Component image={ 'https://3.bp.blogspot.com/-XJrKocsLwIk/VfS6gOoJvJI/AAAAAAAAxRM/oaow0Sye3PM/s800/mokuhyou_tassei_man.png' } alt={'太字'} size={FontSize.Large} /> ); };Componentルール
汎用性を高めるため、同じもの以外はpropsとして扱う。
Component.tsxで作ったpropsはStoriesのreturnで。
今回は2つしか出力していないが、出力する数が多いほどComponentの価値が出る。また、将来的に修正で増える可能性があるためコンポーネントにしておくと管理が楽になる。実行結果
まとめ
先輩に勧められてOOPについての本を読んだとで、コンポーネントの価値理解がより深まった。
ただ既存のコードを修正をするだけでなく、自分で実際に1から作ることの大切さを学べた1週間だった。
- 投稿日:2020-07-27T23:25:19+09:00
React.jsでlocation.hrefを用いた外部URLへの遷移
React.jsにてクリックイベント等でページ遷移させたい場合、
window.location.href
が利用できます。
Vue.jsの場合はwindowオブジェクトからの指定は不要だったため、備忘録として残します。Googleへ遷移させたい場合window.location.href = "https://google.com";reactでの使用例import React from "react"; function Google() { const redirectToGoogle = () => { window.location.href = "https://google.com"; }; return ( <div> <div onClick={redirectToGoogle}>Googleへ</div> </div> ); } export default Google;
- 投稿日:2020-07-27T19:57:48+09:00
emotionを使うメリット
この記事について
社内プロジェクトでstyled-componentsが主に使われており、世間でもstyled-componentsが主流な感じなのですが、
徐々に伸びてきているemotionが気になったので導入して使ってみました。お…!ってなったのでstyled-componentsに比べてよかったこと、悪かったことを簡単にまとめます。
(*随時更新)emotion初心者なのでこんなメリット/デメリットある!ってことがあれば教えてください
emotionとは
後発のCSS-in-Jsのライブラリです。
基本的にはstyled-componentsでできることはほぼできそうな印象を受けました。個人的に良い!って思った点は
- TypeScriptとの相性◎ (https://emotion.sh/docs/typescript)
- render内(html)が見やすくなる// styled-componentっぽく書いたりできます import styled from '@emotion/styled' const Pin = styled.div` color: red; ` render(<Pin> pin component </Pin>)でもこっちの書き方が便利です(TSの恩恵受けられる)
import styled from '@emotion/styled' // Object Stylesっていいます const Pin = css({ color: 'ref', }); render(<div css={Pin}> pin component </div>)他にもいろんな書き方できるみたいです
https://emotion.sh/docs/introductionメリット
TypeScriptと一緒に使ったとき
css構文のサポート受けられる
react書いてるとcss(っぽいの)をかくのめっちゃめんどくさいって思ってました。
これがあればちょっとストレス軽減!!
コンパイル前に間違ったら叱ってくれる
かぶってるやつ教えてくれる
考えずに書いてるとこんなこともあったりするので…
読みやすさ
styled-componentsを使っていると、React Componentとただのタグにstyleつけてるのが混ざって読みにくく感じていました。
緑ばっかりになっててしんどい( <Header> <SiteTitle> ~~ hotel </SiteTitle> <Navigation> <NavigationList> <NavigationItem><a href="/plan">宿泊プラン</a></NavigationItem> <NavigationItem><a href="/price">料金案内</a></NavigationItem> <NavigationItem><a href="/reserve">宿泊予約</a></NavigationItem> <NavigationItem><a href="/access">交通アクセス</a></NavigationItem> </NavigationList> </Navigation> </Header> );emotionを使うといつものhtmlのタグにclassNameっぽくかけるので読みやすいです。
ここにreactComponentが追加されたら、
「あ、これ特別なやつ」ってなるので脳の負担が減るように感じました。return ( <header css={Header}> <h1 css={SiteTitle}> ~~ hotel </h1> <nav css={Navigation}> <ul css={NavigationList}> <li css={NavigationItem}><a href="/plan">宿泊プラン</a></li> <li css={NavigationItem}><a href="/price">料金案内</a></li> <li css={NavigationItem}><a href="/reserve">宿泊予約</a></li> <li css={NavigationItem}><a href="/access">交通アクセス</a></li> </ul> </nav> </header> );他
メディアクエリが簡単
styled-componentsの時は結構書き方めんどくさかったのですが、
emotionでは下記のようにかけます!const Pin = css({ color: 'red', '@media(min-width: 420px)': { color: 'orange' } });デメリット
デメリットもいろいろあるみたいです。
create-react-appとの相性の悪さ
emitして設定を書き換えなきゃいけないのですが、無理矢理感があるので、create-react-appで進めるならstyled-componentsで進めたほうが良い気がします。
(結構めんどくさかったです)経験した中では上記だけです
あと、object stylesはTSの恩恵受けられるので書くのですが、書き方めんどくさい(小声)
- 投稿日:2020-07-27T18:44:21+09:00
react v15 -> v16へマイグレーション
- react v15.6.2 -> v16.13.1
- react-dom v15.6.2 ->v16.13.1
- react-router v3.0.3 -> v5.2.0
- react-router-dom 5.2.0(追加)
- styled-components v2.3.2 -> v5.1.1
- react-responsive v2.0.0 -> v8.1.0
- react-document-meta -> react-helmet(変更)
- history v3.0.0 -> v5.0.0
- react-slick ->v0.27.1
react
componentWillReceiveProps
が廃止予定今後削除される機能なので、ロジックを組み直したり,
componentDidUpdate
に置き換えたりなどの対応が必要です。
今回は急ぎなので、UNSAFE_componentWillReceiveProps
に置き換えます。
componentWillReceivePropsと機能は同じです。後ほどリファクタリングします..!- componentWillReceiveProps(nextProps) { ... + UNSAFE_componentWillReceiveProps(nextProps) {
componentWillMount
が廃止予定
componentWillReceiveProps
と同様です。こちらもUNSAFE_componentWillMount()
にしてあとでリファク(- componentWillMount() { ... + UNSAFE_componentWillMount() {また、自分のコードで
componentWillMount
等の廃止予定ライフサイクルメソッドを使っていなくても、依存パッケージ内で使用されている場合がある。
その場合はパッケージをアップデートするか、アップデート対応していない場合はパッケージの入れ替えを行なった。ルーティング
Link
Link
コンポーネントは react-routerからreact-router-domへ移動されたので、
import元を全て書き換えます。- import { Link } from 'react-router' + import { Link } from 'react-router-dom'browserHistoryの廃止
v4からbroserHistoryが廃止になっているので、置き換えが必要になります。
- <Router history={browserHistory}> - ... - </Router> + <BrowserRouter> + ... + </BrouserRouter>- const hashLocation = browserHistory.getCurrentLocation().hash.replace('#', '') - const hashLocation = this.props.location.hash.replace('#', '')
子コンポーネントでlocationやhistoryを使いたい場合(propsで渡って来ない場合)は、
react-routerがhooks対応しているのでそちらを使えばバケツリレーにならずスッキリかけます。- import React, { Component } from 'react' - class Breadcrumb extends Component { - componentDidMount() { - this.props.dispatch(breadcrumb(browserHistory.getCurrentLocation().pathname)); + import React, { useEffect } from 'react' + const Breadcrumb = (props) => { + const location = useLocation() + useEffect(() => { + props.dispatch(breadcrumb(location.pathname)) + }, [])react-routerのhooksに関しては、下記の記事がわかりやすかったです。
React RouterがHooks対応したので使い方を整理するstore.jsximport { history } from './user' export default function configureStore(initialState) { ... sagaMiddleware.run(rootSaga, { history })styled-components
extend -> styled()
extend
はv4から廃止されていたので、styled(Comp)
の表記に統一します。- const ButtonTertiary = Button.extend` - ... - ` + const ButtonTertiary = styled(Button)` + ... + `injectGlobal -> createGlobalStyle
injectGlobal
というグローバルのCSSの記述が、v4からcreateGlobalStyle
というAPIに取って代わられています。- injectGlobal` -@font-face { - ... + export const GlobalStyle = createGlobalStyle` + @font-face { + ...react-router-scroll
SPAでは何らかの処理をしない限り、ページ遷移後もスクロールの位置が先頭に戻らない。
今回アップデートしたプロジェクトでは、 react-router-scrollが使われていた。
しかし、3年前から更新がかかっていない...
自分で実装した方が早いと判断し、パッケージを捨てることにする。- import { useScroll } from 'react-router-scroll'; + import ScrollTop from './containers/ScrollTop'ルーティング部分<BrowserRouter> <ScrollTop> <Switch> <Route exact path="/" component={Top} /> </Switch> </ScrollTop> </BrowserRouter>ScrollTop.jsximport { useEffect } from 'react' export default function ScrollTop(props) { useEffect(() => { window.scrollTo(0, 0) }, [props.location]) return props.children }実はredux周りなんかも入れたらもっと膨大な作業になりました...
こまめなアップデート、目指していきたいです!参考資料
Styled Components v4について
[React]react-router v4で画面遷移時に前のページのスクロール位置が残る
- 投稿日:2020-07-27T13:29:01+09:00
React公式チュートリアルのクラスコンポーネントを関数コンポーネントに書き替える
なかなか手をつけられなかったReact公式サイトのチュートリアルを、一通りやってみた。
ステートフックuseState()を使って、クラスコンポーネントから関数コンポーネントに書き換えてみたのでメモに残す。公式サイトより:チュートリアルReact の導入
三目並べ完成形hookを呼び出す際に気をつけること => フックを呼び出すのはトップレベルのみ
フックをループや条件分岐、あるいはネストされた関数内で呼び出してはいけません。代わりに、あなたの React の関数のトップレベルでのみ呼び出してください。これを守ることで、コンポーネントがレンダーされる際に毎回同じ順番で呼び出されるということが保証されます。これが、複数回 useState や useEffect が呼び出された場合でも React がフックの状態を正しく保持するための仕組みです。
Square(三目並べの正方形のマス目)
Square.jsximport React from "react"; // function Square(props) { // return ( // <button className="square" onClick={props.onClick}> // {props.value} // </button> // ); // } // Board(盤面) コンポーネントから { onClick, value } = props を受け取っている const Square = ({ onClick, value }) => { return ( <button className="square" onClick={onClick}> {value} </button> ); }; export default Square;Board(盤面)
Board.jsximport React from "react"; // Square(三目並べの正方形のマス目)コンポーネントをimport import Square from "./Square"; // class Board extends React.Component { // renderSquare(i) { // return ( // <Square // value={this.props.squares[i]} // onClick={() => this.props.onClick(i)} // /> // ); // } // Game コンポーネントから { squares, onClick } = props を受け取っている const Board = ({ squares, onClick }) => { const renderSquare = i => { return ( <Square value={squares[i]} onClick={() => { onClick(i); }} /> ); }; // render() { return ( <> <div className="board-row"> {renderSquare(0)} {renderSquare(1)} {renderSquare(2)} </div> <div className="board-row"> {renderSquare(3)} {renderSquare(4)} {renderSquare(5)} </div> <div className="board-row"> {renderSquare(6)} {renderSquare(7)} {renderSquare(8)} </div> </> ); // } }; export default Board;Game
Game(親)コンポーネントは props を使うことで子に情報を渡すことができた。こうすることで、子コンポーネントとの間で常に同期されるようになる。
Game.jsx// useStateをimport import React, { useState } from "react"; // Board(盤面)コンポーネントをimport import Board from "./Board"; import "./style.css"; // class Game extends React.Component { // constructor(props) { // super(props); const Game = () => { // クラスのconstructor内で `this.state` の初期化をやめて `useState` フックを使う // this.state = { // history: [ // { // squares: Array(9).fill(null) // } // ], // useState()を使って、関数コンポーネントに状態を持たせる // const [state変数, set関数] = useState(初期値) const [history, setHistory] = useState([ { squares: Array(9).fill(null) } ]); // stepNumber: 0, const [stepNumber, setStepNumber] = useState(0); // xIsNext: true const [xIsNext, setXIsNext] = useState(true); // }; // } // handleClick(i) { const handleClick = i => { // const history = this.state.history.slice(0, this.state.stepNumber + 1); const historyCurrent = history.slice(0, stepNumber + 1); // const current = history[history.length - 1]; const current = historyCurrent[historyCurrent.length - 1]; // const squares = current.squares.slice(); const squares = [...current.squares]; if (calculateWinner(squares) || squares[i]) { return; } // squares[i] = this.state.xIsNext ? "X" : "O"; squares[i] = xIsNext ? "X" : "O"; //this.setState({ // history: history.concat([ // { // squares: squares // } // ]), setHistory([...historyCurrent, { squares }]); // stepNumber: history.length, setStepNumber(historyCurrent.length); // xIsNext: !this.state.xIsNext setXIsNext(!xIsNext); }; // }); // } // jumpTo(step) { // this.setState({ // stepNumber: step, // xIsNext: (step % 2) === 0 // }); // } const jumpTo = step => { setStepNumber(step); setXIsNext(step % 2 === 0); }; // render() { // const history = this.state.history; // const current = history[this.state.stepNumber]; const current = history[stepNumber]; const winner = calculateWinner(current.squares); const moves = history.map((step, move) => { const desc = move ? `Go to move # ${move}` : `Go to game start`; return ( <li key={move}> {/* <button onClick={() => this.jumpTo(move)}>{desc}</button> */} <button onClick={() => jumpTo(move)}>{desc}</button> </li> ); }); let status; if (winner) { status = `Winner : ${winner}`; } else { // status = "Next player: " + (this.state.xIsNext ? "X" : "O"); status = `Next Player : ${xIsNext ? "X" : "O"}`; } return ( <div className="game"> <div className="game-board"> {/* <Board squares={current.squares} onClick={i => this.handleClick(i)} /> */} <Board squares={current.squares} onClick={i => handleClick(i)} /> </div> <div className="game-info"> <div>{status}</div> <ol>{moves}</ol> </div> </div> ); // } }; const calculateWinner = squares => { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; }; export default Game;参考: React公式サイト
- 投稿日:2020-07-27T06:29:27+09:00
React アプリに Auth0 でシュッと認証を組み込んで Vercel に爆速デプロイする
Auth0 は認証・認可サービスをクラウドで提供している SaaS ベンダーです。
「認証」という機能はどのアプリケーションにも求められる重要な要件ですが、プロダクトの本質的なビジネス価値を持たない場合が多いでしょう。Auth0 を使用することで、この「認証」機能という Undifferentiated Heavy Lifting な作業を排除できます。
本記事では、簡単な React アプリケーションを作成して Auth0 を使用した認証機能を実装します。また、作成したアプリケーションを Vercel(旧 Zeit now)にデプロイする方法を解説します。ユーザのサインアップ後の確認メールなどは SendGrid から送信されます。
以下は、本記事で紹介するアプリケーションの簡単な構成図です。また、本記事で実装されたアプリケーションは Vercel にデプロイしています。こちらからアクセスできます。ソースコードは以下の GitHub リポジトリにホストしています。
https://github.com/daisuke-awaji/auth0-todo-app
React アプリケーションの雛形を作る
まずは
create-react-app
を使用して簡単なアプリケーションを実装していきます。$ create-react-app app --typescriptreact-router
$ yarn add react-router react-router-dom $ yarn add -D @types/react-router @types/react-router-dom最初の react-router はシームレスなナビゲーションを可能にするメインのライブラリです。ふたつめは react-router-dom で、React ルーターの DOM 結合を提供します。
-D
オプションをつけることで開発時にのみ使用するライブラリをインストールできます。この例では TypeScript の型定義ライブラリをインストールしています。react-bootstrap
最低限のデザインを整えるために
react-bootstrap
を使用します。$ yarn add react-bootstrap react-bootstrap-icons bootstrap $ yarn add -D @types/react-bootstrapauth0-react
Auth0 の SDK である
auth0-react
をインストールします。後述しますが、従来のauth0-spa-js
などに比べると Hooks 化の対応が進んでおり、本当に最小限のコードで認証機能を実装できます。$ yarn add @auth0/auth0-reactauth0-react を使用して認証機能を実装する
auth0-react の公式ドキュメント でも解説していますが、本記事ではもう少し実際の開発のユースケースを想定してガイドします。
Auth0Provider
まずは
index.tsx
にて<App/>
コンポーネントを<Auth0Provider/>
でラップします。このようにしておくことで、<App/>
コンポーネント内でuseAuth0
フックを使用できるようになります。useAuth0
フックを使用することで、以下のような様々な認証に関するステートおよびメソッドを取得することができます。
- ステートの例: isLoading, isAuthenticated, user
- メソッドの例: loginWithRedirect, logout
index.tsximport React from "react"; import ReactDOM from "react-dom"; import { Auth0Provider } from "@auth0/auth0-react"; import "bootstrap/dist/css/bootstrap.min.css"; import { App } from "./App"; ReactDOM.render( <Auth0Provider domain={process.env.REACT_APP_AUTH0_DOMAIN!} clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!} redirectUri={window.location.origin} > <App /> </Auth0Provider>, document.querySelector("#root") );domain および、clientId には Auth0 のマネジメントコンソール画面で作成した Application の domain, clientId を入力してください。
LoginButton
ログインボタンは
useAuth0
フックを使用すると簡単に実装できます。loginWithRedirect
メソッドを実行すると Auth0 の SSO エンドポイントを使用してログインした後に、自身のアプリケーションにリダイレクトします。LoginButton.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Button } from "react-bootstrap"; function LoginButton() { const { isAuthenticated, loginWithRedirect } = useAuth0(); return !isAuthenticated ? ( <Button onClick={loginWithRedirect}>Log in</Button> ) : null; } export default LoginButton;実際にログインボタンを押した振る舞いは以下のようになります。
ログイン処理の際、Auth0 のエンドポイントにアクセスし、自身のアプリケーションにリダイレクトします。事前に Application URIs の各種設定をしておきましょう。以下の例では開発用に
localhost:3000
、 本番デプロイ用としてhttps://auth0-todo-app.vercel.app/
を設定しています。LogoutButton
ログアウト処理には
logout
メソッドが用意されています。
ログアウト後に表示するパスをreturnTo: xxxx
で指定します。LogoutButton.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Button } from "react-bootstrap"; function LogoutButton(props: any) { const { isAuthenticated, logout } = useAuth0(); return isAuthenticated ? ( <Button variant="outline-primary" onClick={() => { logout({ returnTo: window.location.origin }); }} {...props} > Log out </Button> ) : null; } export default LogoutButton;UserProfile
ログインユーザの情報は
user
を使用して取得します。認証プロバイダー(Google, Facebook, etc...)によって取得できる情報は異なります。以下では、ログインユーザのアバター画像をuser.picture
として取得しています。UserProfileAvatar.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Image } from "react-bootstrap"; export const UserProfileAvatar = (props: any) => { const { user } = useAuth0(); return ( <Image style={{ width: "30px" }} src={user.picture} roundedCircle {...props} /> ); };この記事のサンプルアプリケーションでは、この情報を組み合わせて、ユーザアイコンをクリックすると各種メニューが表示されるようにしています。
認証済みのユーザだけ見れるページを制御する
react-router-dom を使用した実装ではおなじみの PrivateRoute を組み込んでいきます。
auth0-react
では、ログインしていないユーザが表示しようとすると、ログインページにリダイレクトするwithAuthenticationRequired()
が用意されています。以下のように
ProtectedRoute
のようなルート制御コンポーネントを作成しておくと便利です。ProtectedRoute.tsximport { withAuthenticationRequired } from "@auth0/auth0-react"; import React from "react"; import { Route } from "react-router-dom"; export function ProtectedRoute({ component, ...args }: any) { return ( <Route component={withAuthenticationRequired(component, {})} {...args} /> ); }ルート制御ではこの
ProtectedRoute
コンポーネントを使用して、ログインしていないユーザがアクセスできないページを実装できます。以下の例では、/profile
はログインしていない状態では表示できません。App.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Route, BrowserRouter as Router, Switch } from "react-router-dom"; import Home from "./pages/Home"; import { ProtectedRoute } from "./components/auth/ProtectedRoute"; import Profile from "./pages/Profile"; import { Layout } from "./components/layout/Layout"; import Support from "./pages/Support"; export function App() { const { isLoading } = useAuth0(); if (isLoading) { return <p></p>; } return ( <Router> <Layout> <Switch> <Route path="/" exact component={Home} /> <ProtectedRoute path="/profile" component={Profile} /> <Route path="/support" exact component={Support} /> </Switch> </Layout> </Router> ); }
/profile
にアクセスしようとするとログインページにリダイレクトされています。Vercel にデプロイする
Vercel のダッシュボードから
Import Project
をクリックします。今回は GitHub にホスティングしているソースコードを元にデプロイするので、Git リポジトリを選択します。
最後にビルドコマンドと環境変数を設定します。
Auth0 の domain と clientId は環境変数に指定して React アプリに渡しましょう。あとは Deploy ボタンをクリックして完了です。これだけでデプロイができるはずです。
サインアップ後の確認メールを SendGrid から送信する
Auth0 には Welcome メールやパスワードリセット、アカウントの検証などのためにメールを送信する仕組みが用意されています。
送信するためには事前にメールプロバイダの設定が必要です。今回は無料で提供されている SendGrid を使用してみましょう。
Email Provider の画面から SendGrid を選択し、送信元メールアドレス(From)と SendGrid の API キーを入力します。API キーは SendGrid の settings から発行できます。
ログインに成功すると以下のようなメールが送信されるようになります。
まとめ
Auth0 + Vercel + SendGrid という構成をチュートリアル的に実装し、その振る舞いを確認してみました。
auth0-react
ライブラリは非常に使い勝手がよく、簡単に認証機能を組み込むことができます。一連の流れを実装しても 1 ~ 2 時間で実装できました。以前までは苦労して認証の仕組みを整えていましたが随分と楽になりそうです。
- 投稿日:2020-07-27T06:29:27+09:00
React アプリ に Auth0 でシュッと認証を組み込んで Vercel に爆速デプロイする
Auth0 は認証・認可サービスをクラウドで提供している SaaS ベンダーです。
「認証」という機能はどのアプリケーションにも求められる重要な要件ですが、プロダクトの本質的なビジネス価値を持たない場合が多いでしょう。Auth0 を使用することで、この「認証」機能という Undifferentiated Heavy Lifting な作業を排除できます。
本記事では、簡単な React アプリケーションを作成して Auth0 を使用した認証機能を実装します。また、作成したアプリケーションを Vercel(旧 Zeit now)にデプロイする方法を解説します。ユーザのサインアップ後の確認メールなどは SendGrid から送信されます。
以下は、本記事で紹介するアプリケーションの簡単な構成図です。また、本記事で実装されたアプリケーションは Vercel にデプロイしています。こちらからアクセスできます。ソースコードは以下の GitHub リポジトリにホストしています。
https://github.com/daisuke-awaji/auth0-todo-app
React アプリケーションの雛形を作る
まずは
create-react-app
を使用して簡単なアプリケーションを実装していきます。$ create-react-app app --typescriptreact-router
$ yarn add react-router react-router-dom $ yarn add -D @types/react-router @types/react-router-dom最初の react-router はシームレスなナビゲーションを可能にするメインのライブラリです。ふたつめは react-router-dom で、React ルーターの DOM 結合を提供します。
-D
オプションをつけることで開発時にのみ使用するライブラリをインストールできます。この例では TypeScript の型定義ライブラリをインストールしています。react-bootstrap
最低限のデザインを整えるために
react-bootstrap
を使用します。$ yarn add react-bootstrap react-bootstrap-icons bootstrap $ yarn add -D @types/react-bootstrapauth0-react
Auth0 の SDK である
auth0-react
をインストールします。後述しますが、従来のauth0-spa-js
などに比べると Hooks 化の対応が進んでおり、本当に最小限のコードで認証機能を実装できます。$ yarn add @auth0/auth0-reactauth0-react を使用して認証機能を実装する
auth0-react の公式ドキュメント でも解説していますが、本記事ではもう少し実際の開発のユースケースを想定してガイドします。
Auth0Provider
まずは
index.tsx
にて<App/>
コンポーネントを<Auth0Provider/>
でラップします。このようにしておくことで、<App/>
コンポーネント内でuseAuth0
フックを使用できるようになります。useAuth0
フックを使用することで、以下のような様々な認証に関するステートおよびメソッドを取得することができます。
- ステートの例: isLoading, isAuthenticated, user
- メソッドの例: loginWithRedirect, logout
index.tsximport React from "react"; import ReactDOM from "react-dom"; import { Auth0Provider } from "@auth0/auth0-react"; import "bootstrap/dist/css/bootstrap.min.css"; import { App } from "./App"; ReactDOM.render( <Auth0Provider domain={process.env.REACT_APP_AUTH0_DOMAIN!} clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!} redirectUri={window.location.origin} > <App /> </Auth0Provider>, document.querySelector("#root") );domain および、clientId には Auth0 のマネジメントコンソール画面で作成した Application の domain, clientId を入力してください。
LoginButton
ログインボタンは
useAuth0
フックを使用すると簡単に実装できます。loginWithRedirect
メソッドを実行すると Auth0 の SSO エンドポイントを使用してログインした後に、自身のアプリケーションにリダイレクトします。LoginButton.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Button } from "react-bootstrap"; function LoginButton() { const { isAuthenticated, loginWithRedirect } = useAuth0(); return !isAuthenticated ? ( <Button onClick={loginWithRedirect}>Log in</Button> ) : null; } export default LoginButton;実際にログインボタンを押した振る舞いは以下のようになります。
ログイン処理の際、Auth0 のエンドポイントにアクセスし、自身のアプリケーションにリダイレクトします。事前に Application URIs の各種設定をしておきましょう。以下の例では開発用に
localhost:3000
、 本番デプロイ用としてhttps://auth0-todo-app.vercel.app/
を設定しています。LogoutButton
ログアウト処理には
logout
メソッドが用意されています。
ログアウト後に表示するパスをreturnTo: xxxx
で指定します。LogoutButton.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Button } from "react-bootstrap"; function LogoutButton(props: any) { const { isAuthenticated, logout } = useAuth0(); return isAuthenticated ? ( <Button variant="outline-primary" onClick={() => { logout({ returnTo: window.location.origin }); }} {...props} > Log out </Button> ) : null; } export default LogoutButton;UserProfile
ログインユーザの情報は
user
を使用して取得します。認証プロバイダー(Google, Facebook, etc...)によって取得できる情報は異なります。以下では、ログインユーザのアバター画像をuser.picture
として取得しています。UserProfileAvatar.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Image } from "react-bootstrap"; export const UserProfileAvatar = (props: any) => { const { user } = useAuth0(); return ( <Image style={{ width: "30px" }} src={user.picture} roundedCircle {...props} /> ); };この記事のサンプルアプリケーションでは、この情報を組み合わせて、ユーザアイコンをクリックすると各種メニューが表示されるようにしています。
認証済みのユーザだけ見れるページを制御する
react-router-dom を使用した実装ではおなじみの PrivateRoute を組み込んでいきます。
auth0-react
では、ログインしていないユーザが表示しようとすると、ログインページにリダイレクトするwithAuthenticationRequired()
が用意されています。以下のように
ProtectedRoute
のようなルート制御コンポーネントを作成しておくと便利です。ProtectedRoute.tsximport { withAuthenticationRequired } from "@auth0/auth0-react"; import React from "react"; import { Route } from "react-router-dom"; export function ProtectedRoute({ component, ...args }: any) { return ( <Route component={withAuthenticationRequired(component, {})} {...args} /> ); }ルート制御ではこの
ProtectedRoute
コンポーネントを使用して、ログインしていないユーザがアクセスできないページを実装できます。以下の例では、/profile
はログインしていない状態では表示できません。App.tsximport React from "react"; import { useAuth0 } from "@auth0/auth0-react"; import { Route, BrowserRouter as Router, Switch } from "react-router-dom"; import Home from "./pages/Home"; import { ProtectedRoute } from "./components/auth/ProtectedRoute"; import Profile from "./pages/Profile"; import { Layout } from "./components/layout/Layout"; import Support from "./pages/Support"; export function App() { const { isLoading } = useAuth0(); if (isLoading) { return <p></p>; } return ( <Router> <Layout> <Switch> <Route path="/" exact component={Home} /> <ProtectedRoute path="/profile" component={Profile} /> <Route path="/support" exact component={Support} /> </Switch> </Layout> </Router> ); }
/profile
にアクセスしようとするとログインページにリダイレクトされています。Vercel にデプロイする
Vercel のダッシュボードから
Import Project
をクリックします。今回は GitHub にホスティングしているソースコードを元にデプロイするので、Git リポジトリを選択します。
最後にビルドコマンドと環境変数を設定します。
Auth0 の domain と clientId は環境変数に指定して React アプリに渡しましょう。あとは Deploy ボタンをクリックして完了です。これだけでデプロイができるはずです。
サインアップ後の確認メールを SendGrid から送信する
Auth0 には Welcome メールやパスワードリセット、アカウントの検証などのためにメールを送信する仕組みが用意されています。
送信するためには事前にメールプロバイダの設定が必要です。今回は無料で提供されている SendGrid を使用してみましょう。
Email Provider の画面から SendGrid を選択し、送信元メールアドレス(From)と SendGrid の API キーを入力します。API キーは SendGrid の settings から発行できます。
ログインに成功すると以下のようなメールが送信されるようになります。
まとめ
Auth0 + Vercel + SendGrid という構成をチュートリアル的に実装し、その振る舞いを確認してみました。
auth0-react
ライブラリは非常に使い勝手がよく、簡単に認証機能を組み込むことができます。一連の流れを実装しても 1 ~ 2 時間で実装できました。以前までは苦労して認証の仕組みを整えていましたが随分と楽になりそうです。
- 投稿日:2020-07-27T04:02:35+09:00
[React] React Hooksでどのdependencyが変化したのかわからないだって? そう、そんなときはusePreviousだ!
こんにちは。 @nerikosans です。みなさん、React Hooksしていらっしゃいますでしょうか。
今日は僕がHooksで詰まって3時間溶かして学んだことのメモを書いていこうと思います。悲劇
例えば arrow function があったとして、それをhooksのdepsに入れてしまうと各render毎に異なるオブジェクトとして認識されてしまうのは日本書紀にも記述がありますね。
つまりconst getMySuperBenriFunction = () => { return (a: number, b: number) => a + b; } const benri1 = getMySuperBenriFunction(); const benri2 = getMySuperBenriFunction(); console.log(benri1 === benri2) // => falseであり、
import getMySuperBenriFunction from './getMySuperBenriFunction'; const MySupremeComponent: React.FC = () => { const benri = getMySuperBenriFunction(); const [v1, setV1] = React.useState(1); const [v2, setV2] = React.useState(3); // v1, v2が変わると出力 // benriが変化することは想定してない React.useEffect(() => { console.log(benri(v1, v2)); }, [v1, v2, benri]); return ( <div> (ここにすごいUI) </div> ) }みたいなことをすると全renderで
useEffect
が走って非常につらい気持ちになります。デバッグ策
さて、前述のarrow functionの件はもちろんヤマトタケルの時代から決まっているのですが、うっかりこれを失念してしまったとしましょう。
そんなとき、「なぜこの
useEffect
が走るのかわからない...どのdepsが変化しているんや...」という調査を比較的ラクにできる手法を紹介します。
つまり、変数の値を前サイクルと比較できればよいのです。ということでご登場願いましょう、
usePrevious
さーん!usePreviousさん
function usePrevious<T>(value: T) { const ref = React.useRef<T>(); React.useEffect(() => { ref.current = value; }); return ref.current; }usePreviousさんの勇姿
function usePrevious<T>(value: T) { /*...*/ } const MySupremeComponent: React.FC = () => { /* ... */ console.log(usePrevious(benri) === benri) // => false React.useEffect(() => { console.log(benri(v1, v2)); }, [v1, v2, benri]); return ( { /*...*/ } ) }ということで、
usePrevious
さんを用いて前サイクルと比較すれば、どのdepsの変化でEffectが発火しているのか、比較的早く発見できるでしょう。最高!usePreviousさんの出自
なお、御大はReact公式FAQで紹介されています。公式hookになる可能性もあるみたいです。
おまけ
hooksプロのみなさんには言うまでもないかと思いますが、この問題の解決策は
const getMySuperBenriFunction = () => { return React.useCallback((a: number, b: number) => a + b, []); }と
benri
自体をcallbackに入れることですね。
- 投稿日:2020-07-27T00:57:49+09:00
Ant Design で Menu を含んだ Layout.Header の高さを変更
Ant Design で Menu を含んだ Layout.Header の高さを変更
And Design の Layout にて
Menu
を含んだ実例の記述がありますが、
以下 CSS を適用することで任意の高さに変更します。 (記述は SCSS です)antd-layout-header-height-with-menu// ここで Header 向けの任意の高さを指定 $header-height: 50px; // Header に指定する line-height // calc() を使用しているのは line-height の微調整が必要になるケースがあるため $header-line-height: calc(#{$header-height} + 1px); .ant-layout-header { height: $header-height; line-height: $header-line-height; }詳細
Layout により以下の記述をベースとします。
layout-baseimport React from "react"; import { Layout, Menu } from "antd"; const { Header, Content, Footer } = Layout; export default () => { return ( <Layout className="layout"> <Header> <div className="logo" /> <Menu theme="dark" mode="horizontal" defaultSelectedKeys={["2"]}> <Menu.Item key="1">nav 1</Menu.Item> <Menu.Item key="2">nav 2</Menu.Item> <Menu.Item key="3">nav 3</Menu.Item> </Menu> </Header> <Content style={{ padding: "0 50px" }}>Content</Content> <Footer style={{ textAlign: "center" }}>Footer</Footer> </Layout> ); };Header の height のみを変更
height を変えるので Header に
style={{ height: 50 }}
を指定してみます。height-only・・・ <Header style={{ height: 50 }}> ・・・すると、 Header の高さは変わりましたが Menu がそのままです。
これは以下のように Menu 自体には height は指定されておらず、
Menu の line-height が .ant-layout-header の inherit になっているためのようです。そこで・・・
Header にセレクタを指定したスタイルを適用
Header に対して CSS でのスタイル指定を行います。
header-styled・・・ <Header className="styled"> ・・・
className="styled"
を指定し、 CSS での記述を適用します。header-styled$header-height: 50px; $header-line-height: calc(#{$header-height} + 1px); .ant-layout-header.styled { height: $header-height; line-height: $header-line-height; }これで Header と Menu の高さが調整されました。