簡體   English   中英

React/Redux - 在應用加載/初始化時分派動作

[英]React/Redux - dispatch action on app load/init

我有來自服務器的令牌身份驗證,因此當我的 Redux 應用程序最初加載時,我需要向該服務器發出請求以檢查用戶是否已通過身份驗證,如果是,我應該獲取令牌。

我發現不推薦使用 Redux 核心 INIT 操作,那么在呈現應用程序之前如何調度操作?

您可以在 Root componentDidMount方法中調度操作,在render方法中您可以驗證身份驗證狀態。

像這樣的東西:

class App extends Component {
  componentDidMount() {
    this.props.getAuth()
  }

  render() {
    return this.props.isReady
      ? <div> ready </div>
      : <div>not ready</div>
  }
}

const mapStateToProps = (state) => ({
  isReady: state.isReady,
})

const mapDispatchToProps = {
  getAuth,
}

export default connect(mapStateToProps, mapDispatchToProps)(App)

我對為此提出的任何解決方案都不滿意,然后我想到我正在考慮需要呈現的類。 如果我剛剛創建了一個用於啟動的類,然后將內容推送到componentDidMount方法中,並且只讓render顯示一個加載屏幕呢?

<Provider store={store}>
  <Startup>
    <Router>
      <Switch>
        <Route exact path='/' component={Homepage} />
      </Switch>
    </Router>
  </Startup>
</Provider>

然后有這樣的事情:

class Startup extends Component {
  static propTypes = {
    connection: PropTypes.object
  }
  componentDidMount() {
    this.props.actions.initialiseConnection();
  }
  render() {
    return this.props.connection
      ? this.props.children
      : (<p>Loading...</p>);
  }
}

function mapStateToProps(state) {
  return {
    connection: state.connection
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(Actions, dispatch)
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Startup);

然后編寫一些 redux 操作來異步初始化您的應用程序。 很好用。

這里的所有答案似乎都是創建根組件並在 componentDidMount 中觸發它的變體。 我最喜歡 redux 的一件事是它將數據從組件生命周期中提取出來。 在這種情況下,我看不出有什么不同的理由。

如果你將你的商店導入到根index.js文件中,你可以在那個文件中調度你的動作創建者(我們稱之為initScript() ),它會在加載任何東西之前觸發。

例如:

//index.js

store.dispatch(initScript());

ReactDOM.render(
  <Provider store={store}>
    <Routes />
  </Provider>,
  document.getElementById('root')
);

如果您使用的是 React Hooks,一種單行解決方案是

useEffect(() => store.dispatch(handleAppInit()), []);

空數組確保它只在第一次渲染時被調用一次。

完整示例:

import React, { useEffect } from 'react';
import { Provider } from 'react-redux';

import AppInitActions from './store/actions/appInit';
import store from './store';

export default function App() {
  useEffect(() => store.dispatch(AppInitActions.handleAppInit()), []);
  return (
    <Provider store={store}>
      <div>
        Hello World
      </div>
    </Provider>
  );
}

2020 年更新:與其他解決方案一起,我使用 Redux 中間件來檢查每個請求是否有失敗的登錄嘗試:

export default () => next => action => {
  const result = next(action);
  const { type, payload } = result;

  if (type.endsWith('Failure')) {
    if (payload.status === 401) {
      removeToken();

      window.location.replace('/login');
    }
  }

  return result;
};

2018 年更新:此答案適用於React Router 3

我使用 react-router onEnter道具解決了這個問題。 這是代碼的樣子:

// this function is called only once, before application initially starts to render react-route and any of its related DOM elements
// it can be used to add init config settings to the application
function onAppInit(dispatch) {
  return (nextState, replace, callback) => {
    dispatch(performTokenRequest())
      .then(() => {
        // callback is like a "next" function, app initialization is stopped until it is called.
        callback();
      });
  };
}

const App = () => (
  <Provider store={store}>
    <IntlProvider locale={language} messages={messages}>
      <div>
        <Router history={history}>
          <Route path="/" component={MainLayout} onEnter={onAppInit(store.dispatch)}>
            <IndexRoute component={HomePage} />
            <Route path="about" component={AboutPage} />
          </Route>
        </Router>
      </div>
    </IntlProvider>
  </Provider>
);

使用redux-saga中間件你可以做得很好。

只需定義一個 saga,它在被觸發之前不會監視已調度的動作(例如,使用taketakeLatest )。 當像這樣從 root saga fork ed 時,它將在應用程序啟動時只運行一次。

以下是一個不完整的例子,它需要一些關於redux-saga包的知識,但說明了這一點:

傳奇/launchSaga.js

import { call, put } from 'redux-saga/effects';

import { launchStart, launchComplete } from '../actions/launch';
import { authenticationSuccess } from '../actions/authentication';
import { getAuthData } from '../utils/authentication';
// ... imports of other actions/functions etc..

/**
 * Place for initial configurations to run once when the app starts.
 */
const launchSaga = function* launchSaga() {
  yield put(launchStart());

  // Your authentication handling can go here.
  const authData = yield call(getAuthData, { params: ... });
  // ... some more authentication logic
  yield put(authenticationSuccess(authData));  // dispatch an action to notify the redux store of your authentication result

  yield put(launchComplete());
};

export default [launchSaga];

上述調度代碼launchStartlaunchComplete Redux的動作,你應該創建。 創建這樣的操作是一個很好的做法,因為它們可以派上用場,以便在啟動或完成時通知狀態執行其他操作。

你的 root saga 應該分叉這個launchSaga saga:

傳奇/ index.js

import { fork, all } from 'redux-saga/effects';
import launchSaga from './launchSaga';
// ... other saga imports

// Single entry point to start all sagas at once
const root = function* rootSaga() {
  yield all([
    fork( ... )
    // ... other sagas
    fork(launchSaga)
  ]);
};

export default root;

請閱讀redux-saga非常好的文檔以獲取更多信息。

這是使用 React (16.8) 中最新的 Hooks 的答案:

import { appPreInit } from '../store/actions';
// app preInit is an action: const appPreInit = () => ({ type: APP_PRE_INIT })
import { useDispatch } from 'react-redux';
export default App() {
    const dispatch = useDispatch();
    // only change the dispatch effect when dispatch has changed, which should be never
    useEffect(() => dispatch(appPreInit()), [ dispatch ]);
    return (<div>---your app here---</div>);
}

相似,但是是上述的替代方法(這僅適用於React-Router v3):

Routes.js

import React from 'react';
import { Route, IndexRoute } from 'react-router';

import App from '../components/App';
import Home from '../views/Home';
import OnLoadAuth from '../containers/app/OnLoadAuth';

export default = (
  <Route path="/" component={OnLoadAuth(App)}>
    <IndexRoute component={Home} />
    {* Routes that require authentication *}
  </Route>
);

OnLoadAuth.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

import { authenticateUser, fetchingUser } from '../../actions/AuthActionCreators';
import Spinner from '../../components/loaders/Spinner';

export default App => {
  class OnLoadAuth extends Component {
    componentDidMount = () => this.props.authenticateUser();

    render = () => (
      (this.props.isLoading === undefined || this.props.isLoading)
       ? <Spinner />
       : <App {...this.props} />
    )
 }

 return connect(
    state => ({ isLoading: state.auth.fetchingUser }),
      { authenticateUser, fetchingUser }
    )(OnLoadAuth);
};

我正在使用 redux-thunk 從應用程序初始化的 API 端點獲取用戶下的帳戶,並且它是異步的,因此數據在我的應用程序呈現后進入,並且上面的大多數解決方案對我來說並沒有產生奇跡,有些是貶值。 所以我查看了 componentDidUpdate()。 所以基本上在 APP init 上,我必須有來自 API 的帳戶列表,而我的 redux 商店帳戶將為空或 []。 之后就這樣了。

class SwitchAccount extends Component {

    constructor(props) {
        super(props);

        this.Format_Account_List = this.Format_Account_List.bind(this); //function to format list for html form drop down

        //Local state
        this.state = {
                formattedUserAccounts : [],  //Accounts list with html formatting for drop down
                selectedUserAccount: [] //selected account by user

        }

    }



    //Check if accounts has been updated by redux thunk and update state
    componentDidUpdate(prevProps) {

        if (prevProps.accounts !== this.props.accounts) {
            this.Format_Account_List(this.props.accounts);
        }
     }


     //take the JSON data and work with it :-)   
     Format_Account_List(json_data){

        let a_users_list = []; //create user array
        for(let i = 0; i < json_data.length; i++) {

            let data = JSON.parse(json_data[i]);
            let s_username = <option key={i} value={data.s_username}>{data.s_username}</option>;
            a_users_list.push(s_username); //object
        }

        this.setState({formattedUserAccounts: a_users_list}); //state for drop down list (html formatted)

    }

     changeAccount() {

         //do some account change checks here
      }

      render() {


        return (
             <Form >
                <Form.Group >
                    <Form.Control onChange={e => this.setState( {selectedUserAccount : e.target.value})} as="select">
                        {this.state.formattedUserAccounts}
                    </Form.Control>
                </Form.Group>
                <Button variant="info" size="lg" onClick={this.changeAccount} block>Select</Button>
            </Form>
          );


         }    
 }

 const mapStateToProps = state => ({
      accounts: state.accountSelection.accounts, //accounts from redux store
 });


  export default connect(mapStateToProps)(SwitchAccount);

如果你使用 React Hooks,你可以簡單地使用 React.useEffect 調度一個動作

React.useEffect(props.dispatchOnAuthListener, []);

我使用此模式注冊onAuthStateChanged偵聽器

function App(props) {
  const [user, setUser] = React.useState(props.authUser);
  React.useEffect(() => setUser(props.authUser), [props.authUser]);
  React.useEffect(props.dispatchOnAuthListener, []);
  return <>{user.loading ? "Loading.." :"Hello! User"}<>;
}

const mapStateToProps = (state) => {
  return {
    authUser: state.authentication,
  };
};

const mapDispatchToProps = (dispatch) => {
  return {
    dispatchOnAuthListener: () => dispatch(registerOnAuthListener()),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(App);

與上面提到的 Chris Kemp 相同的解決方案。 可能更通用,只是一個與 redux 無關的 canLift func?

interface Props {
  selector: (state: RootState) => boolean;
  loader?: JSX.Element;
}

const ReduxGate: React.FC<Props> = (props) => {
  const canLiftGate = useAppSelector(props.selector);
  return canLiftGate ? <>{props.children}</> : props.loader || <Loading />;
};

export default ReduxGate;

使用:Apollo Client 2.0、React-Router v4、React 16 (Fiber)

選擇的答案使用舊的 React Router v3。 我需要執行“調度”來加載應用程序的全局設置。 訣竅是使用 componentWillUpdate,雖然示例使用的是 apollo 客戶端,但不獲取解決方案是等效的。 你不需要滾球

設置加載.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import {bindActionCreators} from "redux";
import {
  graphql,
  compose,
} from 'react-apollo';

import {appSettingsLoad} from './actions/appActions';
import defQls from './defQls';
import {resolvePathObj} from "./utils/helper";
class SettingsLoad extends Component {

  constructor(props) {
    super(props);
  }

  componentWillMount() { // this give infinite loop or no sense if componente will mount or not, because render is called a lot of times

  }

  //componentWillReceiveProps(newProps) { // this give infinite loop
  componentWillUpdate(newProps) {

    const newrecord = resolvePathObj(newProps, 'getOrgSettings.getOrgSettings.record');
    const oldrecord = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
    if (newrecord === oldrecord) {
      // when oldrecord (undefined) !== newrecord (string), means ql is loaded, and this will happens
      //  one time, rest of time:
      //     oldrecord (undefined) == newrecord (undefined)  // nothing loaded
      //     oldrecord (string) == newrecord (string)   // ql loaded and present in props
      return false;
    }
    if (typeof newrecord ==='undefined') {
      return false;
    }
    // here will executed one time
    setTimeout(() => {
      this.props.appSettingsLoad( JSON.parse(this.props.getOrgSettings.getOrgSettings.record));
    }, 1000);

  }
  componentDidMount() {
    //console.log('did mount this props', this.props);

  }

  render() {
    const record = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
    return record
      ? this.props.children
      : (<p>...</p>);
  }
}

const withGraphql = compose(

  graphql(defQls.loadTable, {
    name: 'loadTable',
    options: props => {
      const optionsValues = {  };
      optionsValues.fetchPolicy = 'network-only';
      return optionsValues ;
    },
  }),
)(SettingsLoad);


const mapStateToProps = (state, ownProps) => {
  return {
    myState: state,
  };
};

const mapDispatchToProps = (dispatch) => {
  return bindActionCreators ({appSettingsLoad, dispatch }, dispatch );  // to set this.props.dispatch
};

const ComponentFull = connect(
  mapStateToProps ,
  mapDispatchToProps,
)(withGraphql);

export default ComponentFull;

應用程序.js

class App extends Component<Props> {
  render() {

    return (
        <ApolloProvider client={client}>
          <Provider store={store} >
            <SettingsLoad>
              <BrowserRouter>
            <Switch>
              <LayoutContainer
                t={t}
                i18n={i18n}
                path="/myaccount"
                component={MyAccount}
                title="form.myAccount"
              />
              <LayoutContainer
                t={t}
                i18n={i18n}
                path="/dashboard"
                component={Dashboard}
                title="menu.dashboard"
              />

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM