React / Redux-在应用加载/初始化时调度动作


85

我从服务器获得令牌身份验证,因此在最初加载我的Redux应用程序时,我需要向该服务器发出请求,以检查用户是否已通过身份验证,如果是,我应该获得令牌。

我发现不建议使用Redux核心INIT操作,因此如何在呈现应用程序之前调度操作?

Answers:


77

您可以在RootcomponentDidMount方法中调度操作,并且在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)

1
componentWillMount()做的事情。我定义了一个简单的函数,mapDispatchToProps()在App.js中调用所有与调度相关的动作,并在中调用它componentWillMount()
Froxx

很棒,但是使用mapDispatchToProps似乎更具描述性。改为使用mapStateToProps的背后原理是什么?
tcelferact

@ adc17 Oooops :)感谢您的评论。我改变了答案!
Serhii Baraniuk

@SerhiiBaraniuk不用担心。另一件事:假设getAuth是一个动作创建者,我想您想将其定义dispatch为的参数mapDispatchToProps,即const mapDispatchToProps = dispatch => {然后执行以下操作:getAuth: () => { dispatch(getAuth()); }
tcelferact

2
尝试实施此解决方案时遇到此错误Uncaught Error: Could not find "store" in either the context or props of "Connect(App)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(App)".
markhops

35

我对为此提出的任何解决方案都不满意,然后想到我正在考虑需要呈现的类。如果我只是创建了一个用于启动的类,然后将其推送到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操作来异步初始化您的应用程序。工作请客。


现在这就是我一直在寻找的解决方案!我相信您的见解完全正确。谢谢。
YanivGK

26

这里所有的答案似乎都是创建根组件并在componentDidMount中触发它的变体。我最喜欢redux的一件事是,它使数据获取与组件生命周期脱钩。我认为没有理由在这种情况下应该有所不同。

如果要将商店导入到根index.js文件中,则可以initScript()在该文件中调度动作创建者(我们称其为),它将在加载任何内容之前触发。

例如:

//index.js

store.dispatch(initScript());

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

1
我是React的新手,但是在阅读了有关react和redux概念的初始文档之后,我相信这是最合适的方法。在componentDidMount事件上创建这些初始化有什么好处吗?
kuzditomi

1
这真的取决于情况。因此,componentDidMount将在安装特定组件之前触发。射击store.dispatch()ReactDOM.render前()`应用程序挂载前的火灾。componentWillMount对于整个应用程序来说有点像。作为一个新手,我认为最好坚持使用组件生命周期方法,因为它可以使逻辑与使用位置紧密耦合。随着应用程序变得越来越复杂,这变得越来越难做。我的建议是尽可能保持简单。
乔什·皮特曼

1
我最近不得不使用上述方法。我有一个google登录按钮,我需要在应用加载之前触发脚本以使其正常运行。如果我等待应用程序加载然后拨打电话,则它将花费更长的时间来获得响应,并延迟了应用程序的功能。如果在生命周期中处理事务适合您的用例,请坚持使用生命周期。他们更容易考虑。判断此问题的一种好方法是想象自己六个月后的代码。哪种方法将使您更直观地理解。选择这种方法。
乔什·皮特曼

您好@JoshPittman,您仍然需要连接根组件,例如“ App”或类似的组件来订阅redux状态的更新。因此,就像您避免仅从其componentDidMount()方法中调度动作一样。
Tuananhcwrs

1
我对您的调度说是。Redux并没有说我们必须从react组件内部调度动作。Redux当然独立于反应。
Tuananhcwrs

15

如果您使用的是React Hooks,则一种解决方案是:

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

空数组将确保在第一次渲染时仅调用一次。

完整示例:

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

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

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

export default App;

11

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:此答案适用于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>
);

11
为了清楚起见,react-router 4不支持onEnter。
罗布L

IntlProvider应该为您提供一个更好的解决方案的提示。请参阅下面的答案。
克里斯·肯普

这个使用旧的react-router v3,看看我的回答
stackdave

3

借助redux-saga中间件,您可以轻松完成。

只需定义一个传奇,它就不会在触发之前监视已调度的动作(例如,使用taketakeLatest)。当fork从根传奇主编这样说,这将在应用程序启动时运行一次。

以下是一个不完整的示例,它需要一些有关redux-saga软件包的知识,但可以说明这一点:

sagas / 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还原动作。优良作法是在发射开始或完成时,派上用场通知国家做其他事情,以使它们派上用场。

然后,您的根传奇应该对此launchSaga传奇进行分叉:

sagas / 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的非常好的文档以获取有关它的更多信息。


该操作正确完成后才能加载页面吗?
马可夫

1

这是使用最新的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>);
}

0

我正在使用redux-thunk从应用程序初始化时的API端点获取用户下的帐户,并且它是异步的,因此数据在我的应用程序渲染后传入,并且上面的大多数解决方案对我来说都不是什么奇迹,有些是折旧。所以我看了componentDidUpdate()。因此,基本上在APP初始化上,我必须具有API的帐户列表,并且我的redux存储帐户将为null或[]。此后求助于此。

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

0

如果您使用的是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);

-1

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

选择的答案使用旧的React Router v3。我需要执行“调度”以加载应用程序的全局设置。技巧是使用componentWillUpdate,尽管该示例使用的是apollo客户端,但不获取解决方案是等效的。您不需要

SettingsLoad.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

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"
              />

2
该代码不完整,需要对与问题无关的部分进行修整。
Naoise Golden
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.