React / Redux - Versandaktion beim Laden / Init der App

85

Ich habe eine Token-Authentifizierung von einem Server. Wenn meine Redux-App zum ersten Mal geladen wird, muss ich eine Anfrage an diesen Server stellen, um zu überprüfen, ob der Benutzer authentifiziert ist oder nicht. Wenn ja, sollte ich ein Token erhalten.

Ich habe festgestellt, dass die Verwendung von Redux Core INIT-Aktionen nicht empfohlen wird. Wie kann ich also eine Aktion auslösen, bevor die App gerendert wird?

Shota
quelle

Antworten:

77

Sie können eine Aktion in der Root- componentDidMountMethode auslösen und in der renderMethode den Authentifizierungsstatus überprüfen.

Etwas wie das:

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)
Serhii Baraniuk
quelle
1
Für mich componentWillMount()hat das Ding gemacht. Ich habe eine einfache Funktion definiert, die alle mapDispatchToProps()versandbezogenen Aktionen in App.js aufruft und aufruft componentWillMount().
Froxx
Das ist großartig, aber die Verwendung von mapDispatchToProps scheint aussagekräftiger zu sein. Was ist Ihre Begründung für die Verwendung von mapStateToProps?
Tcelferact
@ adc17 Oooops :) danke für den Kommentar. Ich habe meine Antwort geändert!
Serhii Baraniuk
@SerhiiBaraniuk keine Sorge. Eine andere Sache: Unter der Annahme getAuth, dass es sich um dispatcheinen Aktionsersteller handelt mapDispatchToProps, möchten Sie sich als Parameter definieren , dh const mapDispatchToProps = dispatch => {und dann etwas tun wie:getAuth: () => { dispatch(getAuth()); }
tcelferact
2
Ich habe diesen Fehler beim Versuch, diese Lösung zu implementierenUncaught 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

Ich war mit keinen Lösungen zufrieden, die dafür vorgeschlagen wurden, und dann kam mir der Gedanke, dass ich über Klassen nachdachte, die gerendert werden mussten. Was ist, wenn ich gerade eine Klasse für den Start erstellt und dann Dinge in die componentDidMountMethode verschoben habe und die renderAnzeige nur einen Ladebildschirm hat?

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

Und dann haben Sie so etwas:

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

Schreiben Sie dann einige Redux-Aktionen, um Ihre App asynchron zu initialisieren. Arbeitet ein Vergnügen.

Chris Kemp
quelle
Das ist die Lösung, nach der ich gesucht habe! Ich glaube, Ihre Einsicht hier ist vollkommen richtig. Vielen Dank.
YanivGK
26

Alle Antworten hier scheinen Variationen beim Erstellen einer Stammkomponente und beim Auslösen in componentDidMount zu sein. Eines der Dinge, die ich am meisten an Redux mag, ist, dass es das Abrufen von Daten von Komponentenlebenszyklen entkoppelt. Ich sehe keinen Grund, warum es in diesem Fall anders sein sollte.

Wenn Sie Ihren Shop in die Stammdatei importieren index.js, können Sie einfach Ihren Aktionsersteller (nennen wir ihn initScript()) in diese Datei senden, und er wird ausgelöst, bevor etwas geladen wird.

Zum Beispiel:

//index.js

store.dispatch(initScript());

ReactDOM.render(
  <Provider store={store}>
    <Routes />
  </Provider>,
  document.getElementById('root')
);
Josh Pittman
quelle
1
Ich bin ein React-Neuling, aber basierend auf dem Lesen der ersten Dokumentationen zu React- und Redux-Konzepten glaube ich, dass dies der am besten geeignete Weg ist. Gibt es einen Vorteil beim Erstellen dieser Initialisierungen für ein componentDidMountEreignis?
Kuzditomi
1
Es kommt wirklich auf die Situation an. Das componentDidMountwird also ausgelöst, bevor eine bestimmte Komponente montiert wird. Brennen store.dispatch()vor ReactDOM.render () `Feuer vor dem App Mounts. Es ist wie ein componentWillMountfür die gesamte App. Als Neuling denke ich, dass es besser ist, sich an die Methoden des Komponentenlebenszyklus zu halten, da dadurch die Logik eng an den Verwendungsort gekoppelt bleibt. Da Apps immer komplexer werden, wird es schwieriger, dies fortzusetzen. Mein Rat wäre, es so lange wie möglich einfach zu halten.
Josh Pittman
1
Ich musste kürzlich den obigen Ansatz verwenden. Ich hatte eine Google-Anmeldeschaltfläche und musste ein Skript starten, damit es funktioniert, bevor die App geladen wurde. Wenn ich darauf gewartet hätte, dass die App geladen und dann angerufen wird, hätte es nur länger gedauert, bis die Antwort und die Funktionalität in der App verzögert wurden. Wenn es für Ihren Anwendungsfall funktioniert, Dinge in einem Lebenszyklus zu tun, halten Sie sich an die Lebenszyklen. Sie sind einfacher zu denken. Eine gute Möglichkeit, dies zu beurteilen, besteht darin, sich vorzustellen, wie Sie in 6 Monaten den Code betrachten. Welcher Ansatz für Sie intuitiv leichter zu verstehen ist. Wählen Sie diesen Ansatz.
Josh Pittman
Hallo @JoshPittman, Sie müssen noch die Root-Komponente verbinden, z. B. die "App" oder ähnliches, um das Update des Redux-Status zu abonnieren. Es ist also so, als würden Sie vermeiden, Aktionen nur über die componentDidMount () -Methode auszulösen.
Tuananhcwrs
1
Ich sage JA zu Ihrem Punkt über den Versand. Redux sagt nicht, dass wir Aktionen innerhalb einer Reaktionskomponente auslösen müssen. Redux ist sicherlich unabhängig von Reaktionen.
Tuananhcwrs
15

Wenn Sie React Hooks verwenden, wäre eine einzeilige Lösung:

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

Das leere Array würde sicherstellen, dass es beim ersten Rendern nur einmal aufgerufen wird.

Vollständiges Beispiel:

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;
Mathias Berwig
quelle
11

Update 2020 : Zusammen mit anderen Lösungen verwende ich Redux-Middleware, um jede Anforderung auf fehlgeschlagene Anmeldeversuche zu überprüfen:

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 : Diese Antwort gilt für React Router 3

Ich habe dieses Problem mit React -Router onEnter Requisiten gelöst . So sieht Code aus:

// 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>
);
Shota
quelle
11
Nur um klar zu sein, React-Router 4 unterstützt onEnter nicht.
Rob L
Der IntlProvider sollte Ihnen einen Hinweis auf eine bessere Lösung geben. Siehe meine Antwort unten.
Chris Kemp
Dies verwenden alte
React
3

Mit der Redux-Saga- Middleware können Sie das gut machen.

Definieren Sie einfach eine Saga, die nicht auf ausgelöste Aktionen wartet (z. B. mit takeoder takeLatest), bevor sie ausgelöst wird. Wenn es so forkaus der Root-Saga stammt, wird es beim Start der App genau einmal ausgeführt.

Das Folgende ist ein unvollständiges Beispiel, das ein wenig Wissen über das redux-sagaPaket erfordert , aber den Punkt veranschaulicht:

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

Der obige Code löst eine launchStartund launchCompleteRedux-Aktion aus, die Sie erstellen sollten. Es ist eine gute Praxis, solche Aktionen zu erstellen, da sie nützlich sind, um den Staat zu benachrichtigen, wenn der Start gestartet oder abgeschlossen wird, andere Dinge zu tun.

Ihre Wurzelsaga sollte dann diese launchSagaSaga teilen:

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;

Bitte lesen Sie die wirklich gute Dokumentation der Redux-Saga, um weitere Informationen darüber zu erhalten.

Andru
quelle
Seite wird nicht geladen, bis diese Aktion korrekt abgeschlossen ist?
Markov
1

Hier ist eine Antwort mit der neuesten Version von 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>);
}
Eric Blade
quelle
0

Ich habe Redux-Thunk verwendet, um Konten unter einem Benutzer von einem API-Endpunkt auf App Init abzurufen. Es war asynchron, sodass Daten nach dem Rendern meiner App eingingen und die meisten der oben genannten Lösungen keine Wunder für mich taten und einige auch abgeschrieben. Also habe ich mir componentDidUpdate () angesehen. Grundsätzlich musste ich auf APP init Kontenlisten von der API haben, und meine Redux-Speicherkonten wären null oder []. Nachher darauf zurückgegriffen.

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);
Marshall Fungai
quelle
0

Wenn Sie React Hooks verwenden, können Sie eine Aktion einfach mit React.useEffect auslösen

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

Ich benutze dieses Muster für Register onAuthStateChangedListener

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);
BHAR4T
quelle
-1

Verwenden von: Apollo Client 2.0, React-Router v4, React 16 (Glasfaser)

Die ausgewählte Antwort verwendet den alten React Router v3. Ich musste "Versand" durchführen, um die globalen Einstellungen für die App zu laden. Der Trick ist die Verwendung von componentWillUpdate, obwohl das Beispiel den Apollo-Client verwendet und das Abrufen der Lösungen nicht gleichwertig ist. Du brauchst keinen Boucle von

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"
              />
Stackdave
quelle
2
Dieser Code ist unvollständig und muss für die irrelevanten Teile der Frage gekürzt werden.
Naoise Golden