简体   繁体   English

React/Redux - 在应用加载/初始化时分派动作

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

I have token authentication from a server, so when my Redux app is loaded initially I need make a request to this server to check whether user is authenticated or not, and if yes I should get token.我有来自服务器的令牌身份验证,因此当我的 Redux 应用程序最初加载时,我需要向该服务器发出请求以检查用户是否已通过身份验证,如果是,我应该获取令牌。

I have found that using Redux core INIT actions is not recommended, so how can I dispatch an action, before app is rendered?我发现不推荐使用 Redux 核心 INIT 操作,那么在呈现应用程序之前如何调度操作?

You can dispatch an action in Root componentDidMount method and in render method you can verify auth status.您可以在 Root componentDidMount方法中调度操作,在render方法中您可以验证身份验证状态。

Something like this:像这样的东西:

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)

I've not been happy with any solutions that have been put forward for this, and then it occurred to me that I was thinking about classes needing to be rendered.我对为此提出的任何解决方案都不满意,然后我想到我正在考虑需要呈现的类。 What about if I just created a class for startup and then push things into the componentDidMount method and just have the render display a loading screen?如果我刚刚创建了一个用于启动的类,然后将内容推送到componentDidMount方法中,并且只让render显示一个加载屏幕呢?

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

And then have something like this:然后有这样的事情:

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);

Then write some redux actions to async initialise your app.然后编写一些 redux 操作来异步初始化您的应用程序。 Works a treat.很好用。

All of the answers here seem to be variations on creating a root component and firing it in the componentDidMount.这里的所有答案似乎都是创建根组件并在 componentDidMount 中触发它的变体。 One of the things I enjoy most about redux is that it decouples data fetching from component lifecycles.我最喜欢 redux 的一件事是它将数据从组件生命周期中提取出来。 I see no reason why it should be any different in this case.在这种情况下,我看不出有什么不同的理由。

If you are importing your store into the root index.js file, you can just dispatch your action creator(let's call it initScript() ) in that file and it will fire before anything gets loaded.如果你将你的商店导入到根index.js文件中,你可以在那个文件中调度你的动作创建者(我们称之为initScript() ),它会在加载任何东西之前触发。

For example:例如:

//index.js

store.dispatch(initScript());

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

If you are using React Hooks, one single-line solution is如果您使用的是 React Hooks,一种单行解决方案是

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

The empty array ensures it is called only once, on the first render.空数组确保它只在第一次渲染时被调用一次。

Full example:完整示例:

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>
  );
}

Update 2020 : Alongside with other solutions, I am using Redux middleware to check each request for failed login attempts: 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;
};

Update 2018 : This answer is for React Router 3 2018 年更新:此答案适用于React Router 3

I solved this problem using react-router onEnter props.我使用 react-router onEnter道具解决了这个问题。 This is how code looks like:这是代码的样子:

// 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>
);

With the redux-saga middleware you can do it nicely.使用redux-saga中间件你可以做得很好。

Just define a saga which is not watching for dispatched action (eg with take or takeLatest ) before being triggered.只需定义一个 saga,它在被触发之前不会监视已调度的动作(例如,使用taketakeLatest )。 When fork ed from the root saga like that it will run exactly once at startup of the app.当像这样从 root saga fork ed 时,它将在应用程序启动时只运行一次。

The following is an incomplete example which requires a bit of knowledge about the redux-saga package but illustrates the point:以下是一个不完整的例子,它需要一些关于redux-saga包的知识,但说明了这一点:

sagas/launchSaga.js传奇/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];

The code above dispatches a launchStart and launchComplete redux action which you should create.上述调度代码launchStartlaunchComplete Redux的动作,你应该创建。 It is a good practice to create such actions as they come in handy to notify the state to do other stuff whenever the launch started or completed.创建这样的操作是一个很好的做法,因为它们可以派上用场,以便在启动或完成时通知状态执行其他操作。

Your root saga should then fork this launchSaga saga:你的 root saga 应该分叉这个launchSaga saga:

sagas/index.js传奇/ 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;

Please read the really good documentation of redux-saga for more information about it.请阅读redux-saga非常好的文档以获取更多信息。

Here's an answer using the latest in React (16.8), Hooks:这是使用 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>);
}

Similar, but an alternative to the above (this only appears to work with React-Router v3) : 相似,但是是上述的替代方法(这仅适用于React-Router v3):

Routes.js 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 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);
};

I was using redux-thunk to fetch Accounts under a user from an API end-point on app init, and it was async so data was coming in after my app rendered and most of the solutions above did not do wonders for me and some are depreciated.我正在使用 redux-thunk 从应用程序初始化的 API 端点获取用户下的帐户,并且它是异步的,因此数据在我的应用程序呈现后进入,并且上面的大多数解决方案对我来说并没有产生奇迹,有些是贬值。 So I looked to componentDidUpdate().所以我查看了 componentDidUpdate()。 So basically on APP init I had to have accounts lists from API, and my redux store accounts would be null or [].所以基本上在 APP init 上,我必须有来自 API 的帐户列表,而我的 redux 商店帐户将为空或 []。 Resorted to this after.之后就这样了。

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);

If you're using React Hooks, you can simply dispatch an action by using React.useEffect如果你使用 React Hooks,你可以简单地使用 React.useEffect 调度一个动作

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

I use this pattern for register onAuthStateChanged listener我使用此模式注册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);

Same solution as Chris Kemp mentions above.与上面提到的 Chris Kemp 相同的解决方案。 Could be even more generic, just a canLift func not tied to redux?可能更通用,只是一个与 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;

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

The answer selected use old React Router v3.选择的答案使用旧的 React Router v3。 I needed to do 'dispatch' to load global settings for the app.我需要执行“调度”来加载应用程序的全局设置。 The trick is using componentWillUpdate, although the example is using apollo client, and not fetch the solutions is equivalent.诀窍是使用 componentWillUpdate,虽然示例使用的是 apollo 客户端,但不获取解决方案是等效的。 You don't need boucle of你不需要滚球

SettingsLoad.js设置加载.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;

App.js应用程序.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