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

Posted

技术标签:

【中文标题】React/Redux - 在应用程序加载/初始化时调度操作【英文标题】:React/Redux - dispatch action on app load/init 【发布时间】:2016-11-28 13:19:31 【问题描述】:

我有来自服务器的令牌身份验证,所以当我的 Redux 应用程序最初加载时,我需要向该服务器发出请求以检查用户是否经过身份验证,如果是,我应该获取令牌。

我发现不推荐使用 Redux 核心 INIT 动作,那么如何在应用渲染之前调度动作?

【问题讨论】:

【参考方案1】:

您可以在 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)

【讨论】:

对我来说 componentWillMount() 做到了。我在 App.js 的mapDispatchToProps() 中定义了一个简单的函数调用所有与调度相关的操作,并在componentWillMount() 中调用它。 这很好,但使用 mapDispatchToProps 似乎更具描述性。你使用 mapStateToProps 的理由是什么? @adc17 哎呀 :) 感谢您的评论。我改变了答案! @adc17 引用来自doc: [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): If an object is passed, each function inside it is assumed to be a Redux action creator. An object with the same function names, but with every action creator wrapped into a dispatch call so they may be invoked directly, will be merged into the component’s props. 我在尝试实施此解决方案时遇到此错误Uncaught Error: Could not find "store" in either the context or props of "Connect(App)". Either wrap the root component in a &lt;Provider&gt;, or explicitly pass "store" as a prop to "Connect(App)".【参考方案2】:

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 props 解决了这个问题。这就是代码的样子:

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

【讨论】:

要明确 react-router 4 不支持 onEnter。 IntlProvider 应该会给你一个更好的解决方案的提示。请参阅下面的答案。 这个用的是老的 react-router v3,看我的回答【参考方案3】:

我对为此提出的任何解决方案都不满意,然后我突然想到我正在考虑需要渲染类。如果我刚刚创建了一个用于启动的类,然后将内容推送到 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 操作来异步初始化您的应用。工作一种享受。

【讨论】:

这就是我一直在寻找的解决方案!我相信您在这里的见解是完全正确的。谢谢。【参考方案4】:

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

选择的答案使用旧的 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"
              />

【讨论】:

此代码不完整,需要修剪与问题无关的部分。【参考方案5】:

这里的所有答案似乎都是关于创建根组件并在 componentDidMount 中触发它的变体。我最喜欢 redux 的一件事是它将数据获取与组件生命周期分离。我看不出为什么在这种情况下它应该有任何不同。

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

例如:

//index.js

store.dispatch(initScript());

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

【讨论】:

我是一个 react 新手,但根据阅读有关 react 和 redux 概念的初始文档,我相信这是最合适的方式。在 componentDidMount 事件上创建这些初始化有什么好处吗? 这真的取决于情况。因此componentDidMount 将在特定组件安装之前触发。在 ReactDOM.render() 之前触发 store.dispatch() 在应用程序挂载之前触发。这有点像整个应用程序的componentWillMount。作为一个新手,我认为最好坚持使用组件生命周期方法,因为它使逻辑与使用它的位置紧密耦合。随着应用程序变得越来越复杂,这变得越来越难以继续进行。我的建议是尽可能保持简单。 我最近不得不使用上面的方法。我有一个谷歌登录按钮,我需要在应用程序加载之前触发一个脚本以使其工作。如果我等待应用程序加载然后拨打电话,则需要更长的时间才能获得响应,并延迟应用程序中的功能。如果在生命周期中做事适合您的用例,那么请坚持生命周期。它们更容易思考。判断这一点的一个好方法是想象自己在 6 个月后查看代码。您更容易直观地理解哪种方法。选择这种方法。 另外,你真的不需要订阅 redux 上的更新,只需要 dispatch。这就是这种方法的全部要点,我正在利用 redux 将做事情(数据获取、触发操作等)和使用结果(渲染、响应等)解耦的事实。 我同意你关于调度的观点。 Redux 并没有说我们必须从 React 组件内部调度动作。 Redux 肯定是独立于 react 的。【参考方案6】:

使用redux-saga 中间件,您可以做得很好。

只需定义一个 saga,它不会在触发之前监视已调度的操作(例如,使用 taketakeLatest)。当 forked 从 root saga 这样的情况下,它将在应用程序启动时运行一次。

以下是一个不完整的示例,需要对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 redux 操作。创建此类操作是一种很好的做法,因为它们可以在启动或完成时方便地通知状态执行其他操作。

你的根传奇应该分叉这个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;

请阅读真正好的documentation of redux-saga了解更多信息。

【讨论】:

在此操作正确完成之前不会加载页面?【参考方案7】:

如果你使用 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>
  );

【讨论】:

或者,您可以使用import useDispatch from "react-redux"; 然后const dispatch = useDispatch(); 并设置useEffect 来调用dispatch 请参阅react-redux.js.org/api/hooks#usedispatch【参考方案8】:

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

【讨论】:

App 必须在 Provider 下。为了让 TypeScript 满意,我不得不在 dispatch 周围添加一个额外的闭包:useEffect(() => dispatch(AppInit()) , [])。【参考方案9】:

我正在使用 redux-thunk 从应用程序初始化的 API 端点获取用户下的帐户,它是异步的,因此数据在我的应用程序渲染后进入,上面的大多数解决方案对我来说都没有奇迹还有一些是贬值的。所以我查看了componentDidUpdate()。所以基本上在 APP init 上我必须有来自 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);

【讨论】:

【参考方案10】:

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

【讨论】:

【参考方案11】:

与上面提到的 Chris Kemp 相同的解决方案。可以更通用,只是一个不绑定到 redux 的 canLift 函数?

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;

【讨论】:

以上是关于React/Redux - 在应用程序加载/初始化时调度操作的主要内容,如果未能解决你的问题,请参考以下文章

React + Redux - 在初始加载时触发选择onChange()

(通用 React + redux + react-router)如何避免在初始浏览器加载时重新获取路由数据?

React Redux仅从firebase加载一次数据

在 React / Redux 中了解 UI 何时准备就绪

在初始化期间,React / Redux reducer返回undefined

React + redux:连接组件加载时调用调度