React.js:同形/ユニバーサルアプリケーションをゼロから構築します。 パート3:APIを使用して認証とデータ交換を追加する



ログインしてください







これは、同形のReact.jsアプリケーションをゼロから開発することに関する記事の3番目の最後の部分です。 パート12







このパートでは:









1. redux-dev-toolsを追加します



これは、開発プロセスを簡素化する非常に便利なライブラリです。 これにより、グローバル状態の内容とその変化をリアルタイムで確認できます。 さらに、 redux-dev-toolsを使用すると、グローバル状態に対する最新の変更を「ロールバック」できます。これは、テストおよびデバッグ中に便利です。 私たちにとって、それは明快さを追加し、学習プロセスをよりインタラクティブで透明にします。







1.1。 必要なパッケージをインストールする



npm i --save-dev redux-devtools redux-devtools-log-monitor redux-devtools-dock-monitor
      
      





1.2。 redux-dev-toolsパネルのレンダリングを担当するコンポーネントを実装します



src / components / DevTools / DevTools.jsx



 import React from 'react'; import { createDevTools } from 'redux-devtools'; import LogMonitor from 'redux-devtools-log-monitor'; import DockMonitor from 'redux-devtools-dock-monitor'; export default createDevTools( <DockMonitor toggleVisibilityKey='ctrl-h' changePositionKey='ctrl-q'> <LogMonitor /> </DockMonitor> );
      
      





src / components / DevTools / index.js



 import DevTools from './DevTools'; export default DevTools;
      
      





1.3。 「くし」 rootReducer



2番目の部分では、ルートレデューサーの作成をconfigureStoreに配置しましたが 、これは彼の責任範囲ではないため、完全に正しいわけではありません。 少しリファクタリングを行い、それをredux / reducers / index.jsに転送しましょう。







redux /レデューサー/ index.js



 import { combineReducers } from 'redux'; import counterReducer from './counterReducer'; export default combineReducers({ counter: counterReducer });
      
      





redux-dev-toolsのドキュメントから、 configureStoreに変更を加える必要があることがわかります 。 開発にのみredux-dev-toolsが必要であることを思い出してください。したがって、前述の操作を繰り返します。







  1. configureStore.jsの名前をconfigureStore.prod.jsに変更します
  2. configureStore.dev.jsを実装します
  3. configureStore.jsを実装します 。これは、システムランドスケープに応じて、 configureStore.prod.jsまたはconfigureStore.dev.jsを使用します


 mv redux/configureStore.js redux/configureStore.prod.js
      
      





src / redux / configureStore.prod.js



 import { applyMiddleware, createStore } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; export default function (initialState = {}) { return createStore(rootReducer, initialState, applyMiddleware(thunk)); }
      
      





configureTool.dev.jsDevToolsホットリロードのサポートで実装します







src / redux / configureStore.dev.js



 import { applyMiddleware, createStore, compose } from 'redux'; import thunk from 'redux-thunk'; import DevTools from 'components/DevTools'; import rootReducer from './reducers'; export default function (initialState = {}) { const store = createStore(rootReducer, initialState, compose( applyMiddleware(thunk), DevTools.instrument() ) ); if (module.hot) { module.hot.accept('./reducers', () => store.replaceReducer(require('./reducers').default) ); } return store; }
      
      





ConfigureStoreエントリポイント







src / redux / configureStore.js



 if (process.env.NODE_ENV === 'production') { module.exports = require('./configureStore.prod'); } else { module.exports = require('./configureStore.dev'); }
      
      





すべて準備完了です! webpack-dev-servernodemonを再起動し、ブラウザを開いて、右側にグローバルステートの内容を反映するパネルが表示されていることを確認します。 カウンターを含むページを開き、 ReduxCounterクリックます 。 同時に、クリックするたびに、アクションがreduxキューに入力され、グローバル状態がどのように変化するかがわかります。 Revertをクリックすると、最後のアクションを取り消すことができ、 Commitをクリックすると、すべてのアクションを承認し、コマンドの現在のキューをクリアできます。







注: redux-dev-toolsを追加した後、コンソールに次のメッセージが表示される場合があります: "Reactがコンテナー内のマークアップを再利用しようとしましたが、チェックサムが無効でした..." 。 これは、アプリケーションのサーバー側とクライアント側が異なるコンテンツをレンダリングすることを意味します。 これは非常に悪いことであり、アプリケーションでは避ける必要があります。 ただし、この場合、犯人はredux-dev-toolsであり、生産性ではまだ使用しないため、例外を作成して問題に関するメッセージを冷静に無視できます。







更新: gialdeynおよびLerayneユーザーのおかげで、次の方法でredux-dev-toolsでSSRを修正できます







src / server.js



 <div id="react-view">${componentHTML}</div> +++ <div id="dev-tools"></div>
      
      





src / client.js



 +++ import DevTools from './components/DevTools'; ... ReactDOM.render(component, document.getElementById('react-view')); +++ ReactDOM.render(<DevTools store={store} />, document.getElementById('dev-tools'));
      
      





2.新しい機能を追加する



次のシナリオを実装します







  1. ユーザーは「リクエスト時間」ボタンをクリックします。
  2. ダウンロードインジケータを表示します。ボタンが非アクティブになり、不要な繰り返しリクエストを回避します。
  3. アプリケーションはAPIリクエストを作成します。
  4. アプリケーションはAPIから応答を受信し、受信したデータをグローバル状態に保存します。
  5. ダウンロードインジケータが消え、ボタンが再びアクティブになります。 ユーザーが受信したデータを表示します。


これはかなり膨大な作業です。 個々の部分に焦点を当てるために、最初にポイント1、2および5を実装し、3および4に対してスタブを作成します。







2.1。 アクションを追加する



[Request Time]ボタンをクリックした後、次のことを順に行う必要があります。







  1. ロードの値をfalseからtrueに変更します
  2. リクエストをする;
  3. 回答を受け取ったら、 ロードの値をtrueからfalseに戻し、受信したデータまたはエラーに関する情報を保存します。


 export const TIME_REQUEST_STARTED = 'TIME_REQUEST_STARTED'; export const TIME_REQUEST_FINISHED = 'TIME_REQUEST_FINISHED'; export const TIME_REQUEST_ERROR = 'TIME_REQUEST_ERROR'; function timeRequestStarted() { return { type: TIME_REQUEST_STARTED }; } function timeRequestFinished(time) { return { type: TIME_REQUEST_FINISHED, time }; } function timeRequestError(errors) { return { type: TIME_REQUEST_ERROR, errors }; } export function timeRequest() { return (dispatch) => { dispatch(timeRequestStarted()); return setTimeout(() => dispatch(timeRequestFinished(Date.now())), 1000); //  network latency :) }; }
      
      





ここでは各アクションをグローバル状態を変更する小さな関数の形式で配置しtimeRequestはシナリオを完全に説明するこれらの関数の組み合わせです。 コンポーネントから呼び出すのは私たちです。







2.2。 時間の経過とともにページコードを更新します



TimePageページの読み込みインジケーターをサポートするreact-bootstrap-button-loaderボタンを追加し、クリックでtimeRequest関数を呼び出すように教えます。







react-bootstrap-button-loaderパッケージをインストールします



 npm i --save react-bootstrap-button-loader
      
      





ボタンとクリックハンドラーを追加する



src / components / TimePage / TimePage.jsx



 import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import PageHeader from 'react-bootstrap/lib/PageHeader'; import Button from 'react-bootstrap-button-loader'; import { timeRequest } from 'redux/actions/timeActions'; const propTypes = { dispatch: PropTypes.func.isRequired }; class TimePage extends Component { constructor() { super(); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.dispatch(timeRequest()); } render() { return ( <div> <PageHeader>Timestamp</PageHeader> <Button onClick={this.handleClick}>!</Button> </div> ); } } TimePage.propTypes = propTypes; export default connect()(TimePage);
      
      





ボタンがディスパッチ機能にアクセスしてグローバル状態を変更できるように、 react-reduxからの接続を使用する必要があったことに注意してください。







作業の結果を確認します。ブラウザで「時間」ページを開き、「リクエスト」ボタンをクリックします。 インターフェイスはまだ何もしていませんが、 redux-dev-toolsで、最近実装したアクションがどのように起動されるかがわかります。







インターフェースを活性化する時が来ました。 グローバル状態を更新するロジックを実装することから始めましょう







2.3。 減速機を実現



src / redux / reducers / timeReducer.js



 import { TIME_REQUEST_STARTED, TIME_REQUEST_FINISHED, TIME_REQUEST_ERROR } from 'redux/actions/timeActions'; const initialState = { time: null, errors: null, loading: false }; export default function (state = initialState, action) { switch (action.type) { case TIME_REQUEST_STARTED: return Object.assign({}, state, { loading: true, errors: null }); case TIME_REQUEST_FINISHED: return { loading: false, errors: null, time: action.time }; case TIME_REQUEST_ERROR: return Object.assign({}, state, { loading: false, errors: action.errors }); default: return state; } }
      
      





忘れてはならない重要なポイント: redux仕様によれば、渡された状態を変更する権利はなく、それを返すか、新しいオブジェクトを返す必要があります。 新しいオブジェクトを作成するには、 Object.assignを使用します 。これは、元のオブジェクトを取得し 、必要な変更を適用します。







さて、新しいレデューサーをルートレデューサーに追加します。







src / redux / reducers / index.js



 +++ import timeReducer from './timeReducer'; export default combineReducers({ counter: counterReducer, +++ time: timeReducer });
      
      





ブラウザを再度開き、以前にredux-dev-toolsキューをクリアしてから、「リクエスト」ボタンをクリックします。 インターフェイスはまだ更新されていませんが、リデューサーのコードに従ってアクションがグローバル状態を変更します。つまり、「内部」ですべてのロジックが正常に機能することを意味します。 ポイントは小さい-インターフェイスを「復活」させる。







2.4。 ページコード「時間」の更新



src / components / TimePage / TimePage.jsx



 const propTypes = { dispatch: PropTypes.func.isRequired, +++ loading: PropTypes.bool.isRequired, +++ time: PropTypes.any }; class TimePage extends Component { ... render() { +++ const { loading, time } = this.props; ... --- <Button onClick={this.handleClick}>!</Button> +++ <Button loading={loading} onClick={this.handleClick}>!</Button> +++ {time && <div>Time: {time}</div>} </div> ); } } +++ function mapStateToProps(state) { +++ const { loading, time } = state.time; +++ return { loading, time }; +++ } --- export default connect()(TimePage); +++ export default connect(mapStateToProps)(TimePage);
      
      





ブラウザに移動し、「リクエスト」ボタンをクリックして、すべてがシナリオに従って機能することを確認します。







プラグを実際のバックエンドに交換する時が来ました







3. バックエンドと認証との相互作用を追加する



注:この例では、私がRailsで開発した非常にシンプルなバックエンドを使用していますhttps://redux-oauth-backend.herokuapp.comで入手でき、ユーザーがログインしている場合はサーバーのタイムスタンプを返し、そうでない場合は401エラーを返すmethod / test / testが 1つだけ含まれています。 バックエンドのソースコードは、 https//github.com/yury-dymov/redux-oauth-backend-demoにあります 。 そこで、 gem deviseを承認に使用します。これは、 railsgem devise_token_authの同様の問題を解決するための事実上の標準であり、 Bearer Tokenベースの認証 考案承認メカニズムを追加します。 現在、このメカニズムはセキュアなAPIの開発で最もよく使用されています。







クライアント側からは、やるべきことがたくさんあります。







  1. 前の記事から、少し借金が残っています。 サーバー側レンダリング後のグローバル状態 、クライアントによって送信または使用されません。 今すぐ修正します。
  2. フロントエンド認証を担当するredux-oauthライブラリをプロジェクトに追加し、同形スクリプト用に構成します。
  3. スタブを、実際にAPIリクエストを実行するコードに置き換えます。
  4. 「ログイン」ボタンと「ログアウト」ボタンを追加します。


3.1。 グローバル状態を渡します



メカニズムは非常に簡単です。







  1. サーバーがすべての作業を完了し、クライアントのコンテンツを生成した後、 getState関数を呼び出して、現在のグローバル状態を返します。 次に、コンテンツとグローバル状態をHTMLテンプレートに転送し、結果のページをクライアントに渡します。
  2. クライアント側のJavaScriptは、グローバルウィンドウオブジェクトからグローバル状態を直接読み取り、 initialStateとしてconfigureStoreに渡します


src / server.js



 +++ const state = store.getState(); --- return res.end(renderHTML(componentHTML)); +++ return res.end(renderHTML(componentHTML, state)); ... --- function renderHTML(componentHTML, initialState) { +++ function renderHTML(componentHTML, initialState) { <link rel="stylesheet" href="${assetUrl}/public/assets/styles.css"> +++ <script type="application/javascript"> +++ window.REDUX_INITIAL_STATE = ${JSON.stringify(initialState)}; +++ </script> </head>
      
      





src / client.js



 +++ const initialState = window.REDUX_INITIAL_STATE || {}; --- const store = configureStore(); +++ const store = configureStore(initialState);
      
      





コードからわかるように、グローバル状態を変数REDUX_INITIAL_STATEに渡します。







3.2。 承認を追加



redux-oauthをインストールする







注:同型スクリプトにはredux-oauthを使用しますが、クライアント側のみをサポートします 。 さまざまなケースとデモの構成例は、ライブラリのWebサイトで見つけることができます。







注2: redux-oauthは、 ローカルストレージメカニズムが同形のシナリオに適していないため、認証にCookieを使用します。







 npm i --save redux-oauth cookie-parser
      
      





ExpressのcookieParserプラグインを有効にする







src / server.js



 +++ import cookieParser from 'cookie-parser'; const app = express(); +++ app.use(cookieParser());
      
      





アプリケーションのサーバー側のredux-oauthの構成







src / server.js



 +++ import { getHeaders, initialize } from 'redux-oauth'; app.use((req, res) => { const store = configureStore(); +++ store.dispatch(initialize({ +++ backend: { +++ apiUrl: 'https://redux-oauth-backend.herokuapp.com', +++ authProviderPaths: { +++ github: '/auth/github' +++ }, +++ signOutPath: null +++ }, +++ currentLocation: req.url, +++ cookies: req.cookies })).then(() => match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { ... const state = store.getState(); +++ res.cookie('authHeaders', JSON.stringify(getHeaders(state)), { maxAge: Date.now() + 14 * 24 * 3600 * 1000 }); return res.end(renderHTML(componentHTML, state)); }));
      
      





ここで多くの興味深いことが起こります:







  1. redux-oauthからinitialize関数を呼び出す必要があります。この関数には、現在のURLCookie 、および構成(APIアドレスと使用されるOAuthプロバイダー)を渡します
  2. 転送されたCookieに認証トークンが見つかった場合、ライブラリはバックエンドでその有効性を確認し、成功した場合、ユーザー情報をグローバル状態で保存します。 初期化が完了した後にのみ、さらにアプリケーションコードが実行されることに注意してください。
  3. HTMLをクライアントに送信する前に、 res.cookieメソッドを使用します。 このメソッドは、 SetCookieヘッダーをHTTP応答に追加する必要があることを明示的に通知します。このヘッダーでは、更新された認証トークンを転送する必要があります。 これは非常に重要な手順です。新しい認証トークンは、サーバーから応答を受信するとすぐにブラウザーのCookieに保存されます。 したがって、クライアント側のJavaScriptがダウンロード、初期化、または失敗した場合でも、承認が中断しないことを保証します。


ドキュメントによると、 redux-oauthレデューサーをルートレデューサーに追加する必要もあります。







src / redux / reducers / index.js



 +++ import { authStateReducer } from 'redux-oauth'; export default combineReducers({ +++ auth: authStateReducer,
      
      





3.3。 timeActions.jsのスタブを置き換える



src / redux / actions / timeActions.js



 import { fetch, parseResponse } from 'redux-oauth'; export function timeRequest() { return (dispatch) => { dispatch(timeRequestStarted()); --- return setTimeout(() => dispatch(timeRequestFinished(Date.now())), 1000); //  network latency :) +++ return dispatch(fetch('https://redux-oauth-backend.herokuapp.com/test/test')) +++ .then(parseResponse) +++ .then(({ payload }) => dispatch(timeRequestFinished(payload.time))) +++ .catch(({ errors }) => dispatch(timeRequestError(errors))); }; }
      
      





redux-oauthからのフェッチ関数は、 isomorphic-fetchパッケージからの拡張関数です。 ドキュメントによると、 ディスパッチを介して呼び出す必要があります。この場合、グローバルステートにアクセスして、そこから認証トークンを読み取り、リクエストとともに送信できるからです。 APIリクエストではなく、 フェッチ関数が任意のHTTPリクエストに使用される場合、認証トークンは使用されません。つまり、実行アルゴリズムは同形フェッチ実行アルゴリズムと100%同一になります。







注: isomorphic-fetchは、ブラウザー環境とNode環境の両方からHTTP要求を作成できるライブラリです。







ブラウザを開き、「時間」ページの「リクエスト」ボタンを再度クリックします。 さて、現在のタイムスタンプは表示されなくなりましたが、 redux-dev-toolsに 401エラーに関する情報が表示されました。 当然のことながら、APIが何かを返すには、承認が必要です。







3.4。 [ログイン]ボタンと[ログアウト]ボタンを追加します。



原則として、承認されたユーザーはゲストよりもシステムを操作する機会が多くなります。そうでない場合、承認のポイントは何ですか?







技術的な観点から見ると、これは、ユーザーがシステムにログインしているかどうかに応じて、多くのコンポーネントの外観と動作が異なることを意味します。







私はDRYの原則を熱心に支持しています(繰り返さないでください)ので、小さなヘルパーを作成します。







src / redux / models / user.js



 export function isUserSignedIn(state) { return state.auth.getIn(['user', 'isSignedIn']); }
      
      





「ログイン」ボタンを実装します







src / components / AuthButtons / OAuthButton.jsx



 import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { oAuthSignIn } from 'redux-oauth'; import Button from 'react-bootstrap-button-loader'; import { isUserSignedIn } from 'redux/models/user'; const propTypes = { dispatch: PropTypes.func.isRequired, loading: PropTypes.bool.isRequired, provider: PropTypes.string.isRequired, userSignedIn: PropTypes.bool.isRequired }; class OAuthButton extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { const { dispatch, provider } = this.props; dispatch(oAuthSignIn({ provider })); } render() { const { loading, provider, userSignedIn } = this.props; if (userSignedIn) { return null; } return <Button loading={loading} onClick={this.handleClick}>{provider}</Button>; } } OAuthButton.propTypes = propTypes; function mapStateToProps(state, ownProps) { const loading = state.auth.getIn(['oAuthSignIn', ownProps.provider, 'loading']) || false; return { userSignedIn: isUserSignedIn(state), loading }; } export default connect(mapStateToProps)(OAuthButton);
      
      





このボタンは、ユーザーがまだログインしていない場合にのみ表示されます。







「ログアウト」ボタンを実装します







src / components / AuthButtons / SignOutButton.jsx



 import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { signOut } from 'redux-oauth'; import Button from 'react-bootstrap-button-loader'; import { isUserSignedIn } from 'redux/models/user'; const propTypes = { dispatch: PropTypes.func.isRequired, userSignedIn: PropTypes.bool.isRequired }; class SignOutButton extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { const { dispatch } = this.props; dispatch(signOut()); } render() { if (!this.props.userSignedIn) { return null; } return <Button onClick={this.handleClick}></Button>; } } SignOutButton.propTypes = propTypes; function mapStateToProps(state) { return { userSignedIn: isUserSignedIn(state) }; } export default connect(mapStateToProps)(SignOutButton);
      
      





このボタンは、ユーザーが既にログインしている場合にのみ表示されます。







src / components / AuthButtons / index.js



 import OAuthButton from './OAuthButton'; import SignOutButton from './SignOutButton'; export { OAuthButton, SignOutButton };
      
      





HelloWorldPageページに承認を追加します。







src / components / HelloWorldPage / HelloWorldPage.jsx



 +++ import { OAuthButton, SignOutButton } from 'components/AuthButtons'; +++ <h2></h2> +++ <OAuthButton provider='github' /> +++ <SignOutButton />
      
      





私たちの仕事の結果を楽しむ時です。 [ログイン]ボタンをクリックし、GitHubアカウントを使用して認証を行います...システムにいます! [サインイン]ボタンは消えましたが、[サインアウト]ボタンは表示されました。 セッションが保存されていることを確認します。このためにページをリロードします。 [ログアウト]ボタンは消えませんでした。redux-dev-toolsでは、ユーザーに関する情報を見つけることができます。 いいね! これまでのところ、すべてが機能しています。 [時間]ページに移動し、[リクエスト]ボタンをクリックして、 タイムスタンプが表示されていることを確認します。これは、データを返したサーバーです。







これは終了する可能性がありますが、アプリケーションを「研磨」する必要があります。







4.アプリケーションの「研削」



それで、改善できるもの:







  1. [時間]ページへのリンクは、承認されたユーザーにのみ表示する必要があります。
  2. ユーザーがブラウザーで保護されたページのアドレスを入力した場合、それを認証ページ(この場合はHelloWorldPage )にリダイレクトします。
  3. ユーザーがログアウトした場合、グローバル状態からユーザーのデータを削除する必要があります。


4.1。 アクセスできないページへのリンクを削除する



src / components / App / App.jsx



 +++ import { connect } from 'react-redux'; +++ import { isUserSignedIn } from 'redux/models/user'; const propTypes = { +++ userSignedIn: PropTypes.bool.isRequired, ... }; ... +++ {this.props.userSignedIn && ( <LinkContainer to='/time'> <NavItem></NavItem> </LinkContainer> +++ )} ... +++ function mapStateToProps(state) { +++ return { userSignedIn: isUserSignedIn(state) }; +++ } --- export default App; +++ export default connect(mapStateToProps)(App);
      
      





ブラウザーを開いて、「時間」ページへのリンクがまだ使用可能であることを確認し、 HelloWorldPageページに移動して、「終了」ボタンをクリックすると、リンクがなくなります。







4.2。 安全なページへのアクセスを制限する



覚えているように、 react-routerライブラリーは、 URLとレンダリングする必要があるページとの対応を担当し、パス構成はroutes.jsxファイルにあります。 次のロジックを追加する必要があります。ユーザーが承認されておらず、安全なページを要求した場合、それをHelloWorldPageにリダイレクトします。







ユーザーに関する情報を取得するには、グローバルステートリポジトリへのリンクをroutes.jsx渡す必要があります







src / server.js



 --- .then(() => match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { +++ .then(() => match({ routes: routes(store), location: req.url }, (error, redirectLocation, renderProps) => {
      
      





src / client.js



 <Router history={browserHistory}> --- {routes} +++ {routes(store)} </Router>
      
      





src / routes.jsx



 import { isUserSignedIn } from 'redux/models/user'; function requireAuth(nextState, transition, cb) { setTimeout(() => { if (!isUserSignedIn(store.getState())) { transition('/'); } cb(); }, 0); } let store; export default function routes(storeRef) { store = storeRef; return ( <Route component={App} path='/'> <IndexRoute component={HelloWorldPage} /> <Route component={CounterPage} path='counters' /> <Route component={TimePage} path='time' onEnter={requireAuth} /> </Route> ); }
      
      





テスト:







  1. ログインしていることを確認してください。
  2. ブラウザのアドレスバーにhttp:// localhost:3001 / timeと入力し、[Enter]を押すと、ページ[Time]が表示されます。
  3. システムからログアウトします。
  4. http://localhost:3001/time "Enter" — "HelloWorldPage" — !


: requireAuth setTimeout , . , .







4.3.



src/redux/reducers/timeReducer.js



 +++ import { SIGN_OUT } from 'redux-oauth'; +++ case SIGN_OUT: +++ return initialState; default: return state;
      
      





action SIGN_OUT , timeReducer initialState , . , .







5. : Server-Side API Requests



redux-oauth Server Side API requests , API . :









: , , API . redux-oauth .







Proof of Concept .







API







src/server.js



 +++ import { timeRequest } from './redux/actions/timeActions'; ... return store.dispatch(initialize({ backend: { apiUrl: 'https://redux-oauth-backend.herokuapp.com', authProviderPaths: { github: '/auth/github' }, signOutPath: null }, cookies: req.cookies, currentLocation: req.url, })) +++ .then(() => store.dispatch(timeRequest())) .then(() => match({ routes: routes(store), location: req.url }, (error, redirectLocation, renderProps) => {
      
      





, initialize redux-oauth backend , , timeRequest . .







, , "" F5. timestamp , "" . Dev Tools , Network , , API . , .







: API , .







src/redux/actions/timeActions.js



 --- return (dispatch) => { +++ return (dispatch, getState) => { +++ if (!isUserSignedIn(getState())) { +++ return Promise.resolve(); +++ }
      
      





, getState , . , .







6.



- React.js . , !







, .







github — https://github.com/yury-dymov/habr-app/tree/v3







Ps , , . 事前に感謝します!








All Articles