React/Redux-对 app load/init 执行分派操作

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

我发现不推荐使用 Redux 核心 INIT 操作,因此在应用程序呈现之前,如何分派操作?

86849 次浏览

You can dispatch an action in Root componentDidMount method and in render method you can verify auth status.

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)

Update 2020: Alongside with other solutions, I am using Redux middleware to check each request for failed login attempts:

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

I solved this problem using react-router onEnter props. 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>
);

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?

<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. Works a treat.

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

The answer selected use old 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. You don't need boucle of

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

All of the answers here seem to be variations on creating a root component and firing it in the componentDidMount. One of the things I enjoy most about redux is that it decouples data fetching from component lifecycles. 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.

For example:

//index.js


store.dispatch(initScript());


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

With the redux-saga middleware you can do it nicely.

Just define a saga which is not watching for dispatched action (e.g. with take or takeLatest) before being triggered. When forked from the root saga like that it will run exactly once at startup of the app.

The following is an incomplete example which requires a bit of knowledge about the redux-saga package but illustrates the point:

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

The code above dispatches a launchStart and launchComplete redux action which you should create. 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:

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;

Please read the really good documentation of redux-saga for more information about it.

If you are using React Hooks, one single-line solution is

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

Here's an answer using the latest in 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>);
}

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. So I looked to componentDidUpdate(). So basically on APP init I had to have accounts lists from API, and my redux store accounts would be null or []. 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.useEffect(props.dispatchOnAuthListener, []);

I use this pattern for register onAuthStateChanged listener

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. Could be even more generic, just a canLift func not tied to redux?

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;