web-dev-qa-db-fra.com

React / Redux - action de dispatch lors du chargement de l'application / init

J'ai l'authentification par jeton depuis un serveur. Ainsi, lorsque mon application Redux est chargée au départ, j'ai besoin de demander à ce serveur de vérifier si l'utilisateur est authentifié ou non, et si oui, je devrais obtenir un jeton.

J'ai constaté que l'utilisation des actions principales INIT de Redux n'est pas recommandée. Comment puis-je envoyer une action avant le rendu de l'application?

63
Shota

Update : cette réponse concerne React Router 3

J'ai résolu ce problème en utilisant react-router onEnter props. Voici à quoi ressemble le code:

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

Vous pouvez envoyer une action dans la méthode Root componentDidMount et dans la méthode render pour vérifier le statut d'authentification.

Quelque chose comme ça:

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)
63
Serhii Baraniuk

Je n'ai pas été satisfait des solutions qui ont été avancées pour cela, puis il m'est venu à l'esprit que je pensais aux classes qui doivent être rendues. Que se passe-t-il si je viens de créer une classe au démarrage, puis d'insérer des éléments dans la méthode componentDidMount et de faire en sorte que render affiche un écran de chargement?

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

Et puis avoir quelque chose comme ça:

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

Ensuite, écrivez quelques actions redux pour initialiser votre application de manière asynchrone. Fonctionne un régal.

25
Chris Kemp

Toutes les réponses ici semblent être des variations sur la création d'un composant racine et son activation dans le composantDidMount. L'une des choses que j'apprécie le plus dans redux, c'est qu'elle dissocie l'extraction de données du cycle de vie des composants. Je ne vois aucune raison pour que cela soit différent dans ce cas.

Si vous importez votre magasin dans le fichier racine index.js, vous pouvez simplement envoyer votre créateur d'action (appelons-le initScript()) dans ce fichier et il se déclenchera avant que quoi que ce soit ne soit chargé.

Par exemple:

//index.js

store.dispatch(initScript());

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

Si vous utilisez React Hooks, une solution à une seule ligne serait:

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

Le tableau vide garantirait qu'il est appelé une seule fois, lors du premier rendu.

Exemple complet:

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;
4
Mathias Berwig

Avec le middleware redux-saga , vous pouvez le faire facilement.

Définissez simplement une saga qui ne surveille pas l’action déployée (par exemple avec take ou takeLatest) avant d’être déclenchée. Lorsque forked de la saga racine ressemble à cela, il s'exécutera exactement une fois au démarrage de l'application.

Ce qui suit est un exemple incomplet qui nécessite un peu de connaissance sur le paquetage redux-saga mais illustre bien ce 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];

Le code ci-dessus distribue une action launchStart et launchComplete redux que vous devez créer. C'est une bonne pratique de créer de telles actions car elles s'avèrent utiles pour informer l'État de faire d'autres choses à chaque démarrage ou fin du lancement.

Votre saga racine devrait alors créer cette saga 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;

Veuillez lire le très bon documentation de redux-saga pour plus d'informations à ce sujet.

1
Andru

Semblable, mais une alternative à ce qui précède (cela ne semble fonctionner qu'avec 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);
};
1
Matt Carlotta