- 投稿日:2020-07-05T22:01:39+09:00
Amplify で GraphQL と Cognito 認証
概要
はじめての Amplify デプロイまで の続きです。
前回のプロジェクト作成時点で認証に Cognito を使うようにしたり、mock で必要な設定を docker に入れています。
Cognito によるユーザー認証と GraphQL との連携を実装します。ユーザー認証機能の追加
yarn add aws-amplify aws-amplify-reactApp.tsx を以下のように変更します。
アカウント作成時に電話番号を不要にするために withAuthenticator の signUpConfig に hiddenDefaults を追加しています。import React from 'react'; import Amplify from '@aws-amplify/core'; import awsmobile from './aws-exports'; import { withAuthenticator } from 'aws-amplify-react'; import '@aws-amplify/ui/dist/style.css'; Amplify.configure(awsmobile) const App: React.FC = () => { return ( <> <h1>タイトル</h1> </> ) } //@ts-ignore export default withAuthenticator(App, { signUpConfig: { hiddenDefaults: ['phone_number'] } });※ import '@aws-amplify/ui/dist/style.css'; がないと認証画面のスタイルがない状態になるので必要でした
ログイン画面から Create account でアカウントを作成し、メールの認証をするとログインができます。
「タイトル」が表示されます。GraphQL API の作成
まずは自動生成された schema.graphql の TODO モデルを試してみます。
schema.graphqltype Todo @model { id: ID! name: String! description: String }mock を利用することでローカルで検証ができます。
$ amplify mock apihttp://localhost:20002 にアクセスすると以下のように表示されていると思います。
TODO の作成
以下のように createTodo を実行しました。
react と GraphQL の連携
作成したデータを画面に表示してみます。
今回は docker でやっているので aws-exports.js の aws_appsync_graphqlEndpoint を
以下のように localhost に書き換えが必要です(ローカル開発中だけ)aws-exports.js"aws_appsync_graphqlEndpoint": "http://localhost:20002/graphql",App.tsx を以下のように変更します。
Todo に保存したデータの1つを表示します。App.tsximport React, {useState} from 'react' import Amplify from '@aws-amplify/core' import awsmobile from './aws-exports' import {withAuthenticator} from 'aws-amplify-react' import '@aws-amplify/ui/dist/style.css' import {listTodos} from './graphql/queries' import API, {graphqlOperation} from '@aws-amplify/api' import { Auth } from 'aws-amplify'; Amplify.configure(awsmobile) const App: React.FC = () => { const [todos, setTodos] = useState<any>({}) const getTodo = async() => { try { const res: any = await API.graphql(graphqlOperation(listTodos)) console.log(res) setTodos(res.data.listTodos.items) } catch (e) { console.log(e) } } const ShowTodo = () => { if (todos.length > 0) { const todo = todos[0] return ( <> <p>{ todo.id }</p> <p>{ todo.name }</p> <p>{ todo.description }</p> <p>{ todo.createdAt }</p> <p>{ todo.updatedAt }</p> </> ) } return ( <></> ) } return ( <> <h1>Todos</h1> <button onClick={() => getTodo()}>get todos!</button> <div> <ShowTodo/> </div> </> ) } ...get todos! ボタンを押すと下に取得したデータが表示されます。
cognito group による制御
AWS コンソールの Cognito で、グループを作成し、最初のユーザー認証で作成したアカウントに member グループを割り当てます。
schema.graphql を以下のように変更します。
schema.graphqltype Todo @model @auth(rules: [ { allow: owner }, { allow: groups, groups: ["admin"], operations: [read, update] } ]) { id: ID! name: String! description: String }これにより、member 権限のアカウントは Todo に対してなにもできなくなります。
一度ログアウトするために App.tsx を以下のように変更します。App.tsximport React, {useState} from 'react' import Amplify from '@aws-amplify/core' import awsmobile from './aws-exports' import {withAuthenticator} from 'aws-amplify-react' import '@aws-amplify/ui/dist/style.css' import {listTodos} from './graphql/queries' import API, {graphqlOperation} from '@aws-amplify/api' import { Auth } from 'aws-amplify'; Amplify.configure(awsmobile) const App: React.FC = () => { const [todos, setTodos] = useState<any>({}) const signOut = () => { Auth.signOut() } const getTodo = async() => { try { const res: any = await API.graphql(graphqlOperation(listTodos)) console.log(res) setTodos(res.data.listTodos.items) } catch (e) { console.log(e) } } const ShowTodo = () => { if (todos.length > 0) { const todo = todos[0] return ( <> <p>{ todo.id }</p> <p>{ todo.name }</p> <p>{ todo.description }</p> <p>{ todo.createdAt }</p> <p>{ todo.updatedAt }</p> </> ) } return ( <></> ) } return ( <> <button onClick={ () => signOut() }>ログアウト</button> <h1>Todos</h1> <button onClick={() => getTodo()}>get todos!</button> <div> <ShowTodo/> </div> </> ) } ...以下のような画面になります。
一度ログアウトをすると、ログイン画面に遷移するので再度ログインします。
get todos! ボタンを押しても表示は変わらず、データが0件であることがわかります。
例えば、ここで schema.graphql の groups を admin から member に変更すると、データが取得できることを確認できます。また、画面表示をグループごとに分けたいので、App.tsx に以下を追加しました。
初回でログインユーザーのグループ情報を取得し、その情報をもとに表示を出し分けました。
useEffect で初回のみユーザー情報を取得します(第2引数に [] を入れないと無限ループします)App.tsxconst [group, updateGroup] = useState<any>({}) const getUser = async() => { const user = await Auth.currentAuthenticatedUser(); updateGroup(user.signInUserSession.accessToken.payload["cognito:groups"]) console.log(user.signInUserSession.accessToken.payload["cognito:groups"]) } useEffect(() => { getUser() }, []) return ( <> { group === 'admin' ? ( <h1>管理者画面</h1> ) : ( <h1>メンバー画面</h1> ) } <button onClick={ () => signOut() }>ログアウト</button> <h1>Todos</h1> <button onClick={() => getTodo()}>get todos!</button> <div> <ShowTodo/> </div> </> )
- 投稿日:2020-07-05T19:54:23+09:00
日本一わかりやすいReact-Redux入門#4~#7 学習備忘録
はじめに
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux入門』の学習備忘録です。
前回講座で学んだ React に続き、大規模アプリ開発の必須のライブラリである Redux についても勉強をしていきます。
前回の記事はこちら。
#4...Actionsを書いてstateの変更を依頼しよう
- Actionsは、Fluxフローにおいて窓口の役割を果たす。
- アプリから受け取った「stateの変更依頼」をReducersに渡す
- Actionsはプレーンなオブジェクトを返す
src/Reducks/users/action.jsexport const SIGN_IN = "SIGN_IN"; export const signInAction = {userState} => { return { type: "SIGN_IN", payload: { isSignedIn: true, uid: userState.uid, username: userState,username } } }; export const SIGN_OUT = "SIGN_OUT"; export const signOutAction = () => { return { type: "SIGN_OUT", payload: { isSignedIn: false, uid: "", username: "" } } }5...Reducersの作り方とスプレッド構文の使い方
- ReducersはActionsからデータを受け取り、Storeのstateをどう変更するか決める。
- Reducersは、Storeの”現在の状態”と”初期の状態”の情報を保有する。
- 初期状態の定義は、
initialState.js
で行う。src/reducks/store/initialState.jsconst initialState = { users: { isSignedIn: false, uid: "", username: "" } }; export default initialStatesrc/users/reducers.jsimport * as Actions from './actions' import initialState from '../store/initialState' export const UsersReducer = (state = initialState.users, action) => { switch (action.type) { case Actions.SIGN_IN: return { ...state, ...action.payload } default: return state } }
...
はスプレッド構文というjavascriptの文法で、オブジェクトの展開を表す。すなわち、return文は下記と同じ意味になる・return { isSignedIn: false, uid: "", username: "" isSignedIn: true, uid: userState.uid, username: userState,username }同じkeyのものが繰り返された時は、後半にあるものが上書きして残る。
#6...Redux(Store)とReactを接続してstateを変更しよう
- Storeはstateを保存する役割。Reducersにより、Store内部のstateの値を変更される。
src/reducks/store/store.jsimport { createStore as reduxCreateStore, combineReducers, } from 'redux'; import {UsersReducer} from '../users/reducers'; export default function createStore() { return reduxCreateStore( combineReducers({ users: UsersReducer, }) ); }
combineReducers()
は、複数のReducerを束ねる関数。今はUsersReducer
しかないが、Reducerが増えた場合、ここの記述が複数行になる。storeをReactアプリと接続するため、
index.js
を編集する。src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import createStore from './reducks/store/store'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; export const store = createStore(); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
export const store = createStore();
で、Storeを生成。<Provider>
タグで<App />
をラッピングすることで、StoreをReactアプリに渡す。本当にstoreとReactアプリが接続されたのかを、
Redux Hooks
を用いて確認する。App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; import {useDispatch. useSelector} from "react-redux"; function App() { const dispatch = useDispatch() const selector = useSelector((state) => state) console.log(selector.users) return ( ...localhost:3000をGoogle Chromeで開き、検証ツールから
console.log()
の結果を見に行く。
initialState
として定義しているuserオブジェクトの初期値が、Reactアプリに渡っているのが分かります!次に、このstateの値を更新してみます。
src/reducks/users/actions.js
で定義したsignInAction()
が発火するボタンをApp.js
内に追記します。src/App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; import {useDispatch, useSelector} from "react-redux"; import {signInAction} from "./reducks/users/actions" function App() { const dispatch = useDispatch() const selector = useSelector((state) => state) console.log(selector.users) return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <button onClick={() => dispatch(signInAction({uid: "0001", username: "torahack"})) } > Sign In </button> </header> </div> ); } export default App;
<button>
タグのonClick
イベントに、先ほどのsignInAction()
を渡しています。このボタンをクリックすると、stateの値が更新されているのが分かります!
#7...URLに応じたコンポーネントを表示しよう
connected-react-routerというReduxライブラリを使用する。これは、Reduxのstoreを利用してルーティングを管理するというもの。
store.js
とindex.js
を編集する。src/reducks/store/store/jsimport { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux'; import {connentReactRouter, routerMiddleware} from "connected-react-router"; import {UsersReducer} from '../users/reducers'; export default function createStore(history) { return reduxCreateStore( combineReducers({ router: connentReactRouter(histroy), users: UsersReducer, }), applyMiddleware( routerMiddleware(history) ) ); }
createStore()
が新たにhistory
という引数を受け取っている。これは、サイト遷移の履歴が入っているrouter
というstate名で、このhistory
を管理する。src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import createStore from './reducks/store/store'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import * as History from 'history'; const histroy = History.createBrowserHistory(); export const store = createStore(history); ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>, document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
<ConnectedRouter>
でさらに<App />
をラッピング。具体的なルーティングの定義を実装する、実装は、
Router.jsx
という特殊なコンポーネントファイルを作成して行う。src/Router.jsximport React from 'react'; import {Route, Switch} from "react-router"; import {Login, Home} from "templates"; const Router = () => { return ( <Switch> <Route exact path="/login" component={Login} /> <Route exact path="(/)?" component={Home} /> </Switch> ); }; export default Router
<Route>
タグ内で、パスとコンポーネントの対応を記述する。exact path=
が完全一致、path=
が部分一致。静的なルーティングに対しては前者を使い、動的なルーティングに対しては後者を使う("/posts/:id"のようなケース)この時点では、templatesファイルは未作成。これを作る前に、
App.js
を変更する。拡張子をApp.jsx
にした上で、中身をがらっと変える。src/App.jsximport React from 'react' import Router from './Router' const App = () => { <main> <Router /> </main> }; export default App;
<main>
タグでRouterコンポーネントを呼び出している。この<Router />
の中身が、パスごとに切り替わるイメージ続いて、templatesファイルを作る。templatesファイルは、静的ページにつき1個ずつ存在し、そのページ(ルーティング)において親コンポーネントの役割を果たす。
作成するファイルは以下の通り。
- src/templates/Login.jsx
- src/templates/Home.jsx
- src/templates/index.js
index.js
はエントリーポイントの役割を果たす。src/templates/Login.jsximport React from 'react'; import {useDispatch} from "react-redux"; import {push} from "connected-react-router"; const Login = () => { const dispatch = useDispatch(); return ( <div> <h2>ログイン</h2> <button onClick={() => dispatch(push('/'))}> ログインする </button> </div> ); }; export default Login
useDispatch()
で store に紐付いた dispatch が取得できる。push()
を実行すると、引数のパスにページを遷移させることができる。現時点では、ダミーとしてルートパスを指定。src/templates/Home.jsximport React from 'react'; const Home = () => { return ( <h2>Home</h2> ); }; export default Homesrc/templates/index.jsexport {default as Login} from './Login' export {default as Home} from './Home'ブラウザで確認すると、
path = "/" path = "/login" 上手く表示されています!また、ログインボタンを押すことで、Home画面へ遷移できるようになっています。
おわり
今回はここまで。
第三回講座で概念を学んだFluxフローについて、実際に手を動かすことでなんとなーく分かってきた気がしてきました。
開発フレームワークにおける「どこに何を書くべきかが決まっている」という状態は、初期の学習コストは高くなりがちな反面、慣れることさえできれば、開発だけでなく保守・改修も効率的に行うことができる、というメリットがあると思います。Reduxもその類かと思われるので、頑張って習得していきたいです。
次回記事は今後更新予定。
- 投稿日:2020-07-05T19:54:23+09:00
日本一わかりやすいReact-Redux入門#4~#7
はじめに
この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux入門』の学習備忘録です。
前回講座で学んだ React に続き、大規模アプリ開発の必須のライブラリである Redux についても勉強をしていきます。
前回の記事はこちら。
#4...Actionsを書いてstateの変更を依頼しよう
- Actionsは、Fluxフローにおいて窓口の役割を果たす。
- アプリから受け取った「stateの変更依頼」をReducersに渡す
- Actionsはプレーンなオブジェクトを返す
src/Reducks/users/action.jsexport const SIGN_IN = "SIGN_IN"; export const signInAction = {userState} => { return { type: "SIGN_IN", payload: { isSignedIn: true, uid: userState.uid, username: userState,username } } }; export const SIGN_OUT = "SIGN_OUT"; export const signOutAction = () => { return { type: "SIGN_OUT", payload: { isSignedIn: false, uid: "", username: "" } } }5...Reducersの作り方とスプレッド構文の使い方
- ReducersはActionsからデータを受け取り、Storeのstateをどう変更するか決める。
- Reducersは、Storeの”現在の状態”と”初期の状態”の情報を保有する。
- 初期状態の定義は、
initialState.js
で行う。src/reducks/store/initialState.jsconst initialState = { users: { isSignedIn: false, uid: "", username: "" } }; export default initialStatesrc/users/reducers.jsimport * as Actions from './actions' import initialState from '../store/initialState' export const UsersReducer = (state = initialState.users, action) => { switch (action.type) { case Actions.SIGN_IN: return { ...state, ...action.payload } default: return state } }
...
はスプレッド構文というjavascriptの文法で、オブジェクトの展開を表す。すなわち、return文は下記と同じ意味になる・return { isSignedIn: false, uid: "", username: "" isSignedIn: true, uid: userState.uid, username: userState,username }同じkeyのものが繰り返された時は、後半にあるものが上書きして残る。
#6...Redux(Store)とReactを接続してstateを変更しよう
- Storeはstateを保存する役割。Reducersにより、Store内部のstateの値を変更される。
src/reducks/store/store.jsimport { createStore as reduxCreateStore, combineReducers, } from 'redux'; import {UsersReducer} from '../users/reducers'; export default function createStore() { return reduxCreateStore( combineReducers({ users: UsersReducer, }) ); }
combineReducers()
は、複数のReducerを束ねる関数。今はUsersReducer
しかないが、Reducerが増えた場合、ここの記述が複数行になる。storeをReactアプリと接続するため、
index.js
を編集する。src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import createStore from './reducks/store/store'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; export const store = createStore(); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
export const store = createStore();
で、Storeを生成。<Provider>
タグで<App />
をラッピングすることで、StoreをReactアプリに渡す。本当にstoreとReactアプリが接続されたのかを、
Redux Hooks
を用いて確認する。App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; import {useDispatch. useSelector} from "react-redux"; function App() { const dispatch = useDispatch() const selector = useSelector((state) => state) console.log(selector.users) return ( ...localhost:3000をGoogle Chromeで開き、検証ツールから
console.log()
の結果を見に行く。
initialState
として定義しているuserオブジェクトの初期値が、Reactアプリに渡っているのが分かります!次に、このstateの値を更新してみます。
src/reducks/users/actions.js
で定義したsignInAction()
が発火するボタンをApp.js
内に追記します。src/App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; import {useDispatch, useSelector} from "react-redux"; import {signInAction} from "./reducks/users/actions" function App() { const dispatch = useDispatch() const selector = useSelector((state) => state) console.log(selector.users) return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> <button onClick={() => dispatch(signInAction({uid: "0001", username: "torahack"})) } > Sign In </button> </header> </div> ); } export default App;
<button>
タグのonClick
イベントに、先ほどのsignInAction()
を渡しています。このボタンをクリックすると、stateの値が更新されているのが分かります!
#7...URLに応じたコンポーネントを表示しよう
connected-react-routerというReduxライブラリを使用する。これは、Reduxのstoreを利用してルーティングを管理するというもの。
store.js
とindex.js
を編集する。src/reducks/store/store/jsimport { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux'; import {connentReactRouter, routerMiddleware} from "connected-react-router"; import {UsersReducer} from '../users/reducers'; export default function createStore(history) { return reduxCreateStore( combineReducers({ router: connentReactRouter(histroy), users: UsersReducer, }), applyMiddleware( routerMiddleware(history) ) ); }
createStore()
が新たにhistory
という引数を受け取っている。これは、サイト遷移の履歴が入っているrouter
というstate名で、このhistory
を管理する。src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux'; import createStore from './reducks/store/store'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import * as History from 'history'; const histroy = History.createBrowserHistory(); export const store = createStore(history); ReactDOM.render( <Provider store={store}> <ConnectedRouter history={history}> <App /> </ConnectedRouter> </Provider>, document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
<ConnectedRouter>
でさらに<App />
をラッピング。具体的なルーティングの定義を実装する、実装は、
Router.jsx
という特殊なコンポーネントファイルを作成して行う。src/Router.jsximport React from 'react'; import {Route, Switch} from "react-router"; import {Login, Home} from "templates"; const Router = () => { return ( <Switch> <Route exact path="/login" component={Login} /> <Route exact path="(/)?" component={Home} /> </Switch> ); }; export default Router
<Route>
タグ内で、パスとコンポーネントの対応を記述する。exact path=
が完全一致、path=
が部分一致。静的なルーティングに対しては前者を使い、動的なルーティングに対しては後者を使う("/posts/:id"のようなケース)この時点では、templatesファイルは未作成。これを作る前に、
App.js
を変更する。拡張子をApp.jsx
にした上で、中身をがらっと変える。src/App.jsximport React from 'react' import Router from './Router' const App = () => { <main> <Router /> </main> }; export default App;
<main>
タグでRouterコンポーネントを呼び出している。この<Router />
の中身が、パスごとに切り替わるイメージ続いて、templatesファイルを作る。templatesファイルは、静的ページにつき1個ずつ存在し、そのページ(ルーティング)において親コンポーネントの役割を果たす。
作成するファイルは以下の通り。
- src/templates/Login.jsx
- src/templates/Home.jsx
- src/templates/index.js
index.js
はエントリーポイントの役割を果たす。src/templates/Login.jsximport React from 'react'; import {useDispatch} from "react-redux"; import {push} from "connected-react-router"; const Login = () => { const dispatch = useDispatch(); return ( <div> <h2>ログイン</h2> <button onClick={() => dispatch(push('/'))}> ログインする </button> </div> ); }; export default Login
useDispatch()
で store に紐付いた dispatch が取得できる。push()
を実行すると、引数のパスにページを遷移させることができる。現時点では、ダミーとしてルートパスを指定。src/templates/Home.jsximport React from 'react'; const Home = () => { return ( <h2>Home</h2> ); }; export default Homesrc/templates/index.jsexport {default as Login} from './Login' export {default as Home} from './Home'ブラウザで確認すると、
path = "/" path = "/login" 上手く表示されています!また、ログインボタンを押すことで、Home画面へ遷移できるようになっています。
おわり
今回はここまで。
第三回講座で概念を学んだFluxフローについて、実際に手を動かすことでなんとなーく分かってきた気がしてきました。
開発フレームワークにおける「どこに何を書くべきかが決まっている」という状態は、初期の学習コストは高くなりがちな反面、慣れることさえできれば、開発だけでなく保守・改修も効率的に行うことができる、というメリットがあると思います。Reduxもその類かと思われるので、頑張って習得していきたいです。
次回記事は今後更新予定。
- 投稿日:2020-07-05T19:46:31+09:00
How to Install React JS in Laravel 7 with Bootstrap
If you want to build a React application with Laravel, then the first and foremost thing is you must know how to install React in Laravel 7. We will use laravel/ui Package to install react in laravel with Bootstrap 4.
click here to read more:
https://www.positronx.io/how-to-install-react-js-in-laravel-with-bootstrap/
- 投稿日:2020-07-05T18:17:14+09:00
SPA(React) + Firebase Authentication でリロード時もログイン情報を維持できるようにする
ちょっとハマったので備忘録
ここで紹介する方法以外でベストプラクティス等あればコメントで教えていただけると嬉しいですFirebase Authentication + SPA でよくある設計
Firebase Authentication を使ったログイン機能付き SPA の実装例をググったときに見かける設計として
「認証が必要なページにアクセスされたときは、ログインしていればそのまま表示し、ログインしていなければログインページへ飛ばす」
というのがよくあるパターンだと思います。例えば React Router を使うと次のような実装が考えられます。
※ React Router ドキュメントのコードをベースにしています https://reactrouter.com/web/example/auth-workflowconst PrivateRoute: React.FC<RouteProps> = ({ children, ...rest }) => { // getUser は保持されたユーザー情報を取得するための何らかの関数です。実装は省略 // 後で登場する setUser 関数で保持したユーザー情報を取り出します // state を使ってもいいし、Redux 等のストアを使ってもいいし、Context を使っても構いません const user = getUser(); return ( <Route {...rest} render={({ location }) => { if (user) { return children; } else { return <Redirect to={{ pathname: 'login', state: { from: location } }} />; } }} /> ); }; export default function App() { useEffect(() => { firebase.auth().onAuthStateChanged((user) => { if (user) { // setUser はユーザー情報を保持しておくための何らかの関数です。実装は省略 // state を使ってもいいし、Redux 等のストアを使ってもいいし、Context を使っても構いません setUser(user); } }); }, []); return ( <> <Router> <Link to="/public">ログインが不要なページ</Link> <Link to="/private">ログインが必要なページ</Link> <Switch> <Route path="/login"> <> <div>ログインページです</div> {/* Firebase Authentication へのログイン方法は何でも OK ですが */} {/* ここでは StyledFirebaseAuth を使ってログイン画面を表示します */} <StyledFirebaseAuth ... /> </> </Route> <Route path="/public"> <div>このページは誰でも見ることができます</div> </Route> <PrivateRoute path="/private"> <div>このページはログインしたユーザーだけが見ることができます</div> </PrivateRoute> </Switch> </Router> </> ); }この実装で基本的には問題ないです。トップページ
/
から/public
に遷移するとログインの有無に関係なくページが表示され、/private
に遷移するとログインしていれば表示されるし、ログインしていなければログインページへリダイレクトされます問題点
ページ遷移でアプリケーション内をぐるぐるする分には問題ないのですが、
/private
にいる状態でページをリロードしてみると先程ログインしたのにまたログインページへ飛ばされてしまいます
Firebase Authentication 的にはログイン状態ではあるのですが、firebase.auth().onAuthStateChanged
は非同期で走るので、/private
にダイレクトにアクセスするとsetUser
がされる前にgetUser
が実行されてしまいます
setUser
されてないのでユーザー情報は存在せず、ログインされていない判定になりログインページへ再び飛ばされてしまいます解決方法
実装を工夫してあげる必要があります
解決方法はシンプルでonAuthStateChanged
が終わってからページを表示するかログインページへ飛ばすかどうかを確定させるアプローチで対処できます
(ここでは React Router を使った場合の解決方法を書いていますが、他のルーターの場合でもミドルウェアを使って似たようなことができると思います)
PrivateRoute
を次のように書き換えてあげますconst PrivateRoute: React.FC<RouteProps> = ({ children, ...rest }) => { const [authChecked, setAuthChecked] = useState(false); useEffect(() => { firebaseAuth.onAuthStateChanged((user) => { if (user) { setUser(user); } setAuthChecked(true); }); }, []); const user = getUser(); return ( <Route {...rest} render={({ location }) => { if (authChecked) { if (user) { return children; } else { return <Redirect to={{ pathname: 'login', state: { from: location } }} />; } } else { return <></>; } }} /> ); };「ログイン状態の確認が終わったか」「ログインしているかどうか」の2段階で分岐することで UX を損なうことなくリロード問題に対処できるようになりました
- 投稿日:2020-07-05T17:04:40+09:00
僕が考えた最強の React アーキテクチャ
Component
Page
Container と Component を組み合わせてページを構成する
Container, Component
Container → Container Components
Component → Presentational Componentshttps://redux.js.org/basics/usage-with-react#presentational-and-container-components
Store
Action, Reducer, State, Selector
Facade
https://ja.wikipedia.org/wiki/Facade_パターン
Service
- 投稿日:2020-07-05T13:56:09+09:00