React / Redux: invia l'azione al caricamento / inizializzazione dell'app


88

Ho l'autenticazione del token da un server, quindi quando la mia app Redux viene caricata inizialmente, devo fare una richiesta a questo server per verificare se l'utente è autenticato o meno, e se sì dovrei ottenere il token.

Ho scoperto che l'utilizzo delle azioni INIT di Redux core non è consigliato, quindi come posso inviare un'azione prima che l'app venga visualizzata?

Risposte:


78

Puoi inviare un'azione nel componentDidMountmetodo Root e nel rendermetodo puoi verificare lo stato di autenticazione.

Qualcosa come questo:

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
Per me ha componentWillMount()fatto la cosa. Ho definito una semplice funzione che chiama tutte le azioni relative al dispatch in mapDispatchToProps()App.js e l'ho chiamata componentWillMount().
Froxx

Questo è fantastico, ma l'uso di mapDispatchToProps sembra più descrittivo. Qual è la tua logica dietro l'utilizzo di mapStateToProps invece?
tcelferact

@ adc17 Oooops :) grazie per il commento. Ho cambiato la mia risposta!
Serhii Baraniuk

1
@ adc17 citazione da 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.
Serhii Baraniuk

2
Ho ricevuto questo errore durante il tentativo di implementare questa soluzioneUncaught 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

Non sono stato contento delle soluzioni proposte per questo, e poi mi è venuto in mente che stavo pensando alle classi che necessitano di essere renderizzate. E se avessi appena creato una classe per l'avvio e poi inserissi le cose nel componentDidMountmetodo e avessi appena rendervisualizzato una schermata di caricamento?

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

E poi fai qualcosa del genere:

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

Quindi scrivi alcune azioni di redux per inizializzare in modo asincrono la tua app. Funziona a meraviglia.


Questa è la soluzione che stavo cercando! Credo che la tua intuizione qui sia perfettamente giusta. Grazie.
YanivGK

29

Tutte le risposte qui sembrano essere variazioni sulla creazione di un componente radice e sull'attivazione nel componentDidMount. Una delle cose che mi piace di più del redux è che disaccoppia il recupero dei dati dai cicli di vita dei componenti. Non vedo motivo per cui dovrebbe essere diverso in questo caso.

Se stai importando il tuo negozio nel index.jsfile root , puoi semplicemente inviare il tuo creatore di azioni (chiamiamolo initScript()) in quel file e si attiverà prima che venga caricato qualcosa.

Per esempio:

//index.js

store.dispatch(initScript());

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

1
Sono un principiante di React, ma sulla base della lettura della documentazione iniziale sui concetti di React e Redux, credo che questo sia il modo più appropriato. c'è qualche vantaggio nel creare queste inizializzazioni su un componentDidMountevento?
kuzditomi

1
Dipende davvero dalla situazione. Quindi si componentDidMountattiverà prima del montaggio di un componente specifico. Firing store.dispatch()before ReactDOM.render () `si attiva prima che l'app venga montata. È una specie di componentWillMountper l'intera app. Come principiante, penso che sia meglio attenersi all'uso dei metodi del ciclo di vita dei componenti perché mantiene la logica strettamente collegata a dove viene utilizzata. Man mano che le app diventano sempre più complesse, diventa più difficile continuare a farlo. Il mio consiglio sarebbe di mantenerlo semplice il più a lungo possibile.
Josh Pittman,

1
Ho dovuto usare l'approccio sopra di recente. Avevo un pulsante di accesso a Google e avevo bisogno di attivare uno script per farlo funzionare prima che l'app si caricasse. Se avessi aspettato che l'app si caricasse e poi effettuassi la chiamata, ci sarebbe voluto più tempo per ottenere la risposta e la funzionalità nell'app potrebbe ritardare. Se fare le cose in un ciclo di vita funziona per il tuo caso d'uso, segui i cicli di vita. Sono più semplici da pensare. Un buon modo per giudicare questo è immaginarti mentre guardi il codice tra 6 mesi. Quale approccio sarebbe più facile da capire intuitivamente. Scegli quell'approccio.
Josh Pittman,

1
Inoltre, non hai davvero bisogno di iscriverti all'aggiornamento su redux, devi solo inviare. Questo è il punto centrale di questo approccio, sto approfittando del fatto che redux disaccoppia l'esecuzione della cosa (recupero dei dati, attivazione di un'azione, ecc.) E l'utilizzo del risultato (rendering, risposta, ecc.).
Josh Pittman,

2
Dico SÌ al tuo punto di vista sulla spedizione. Redux non dice che dobbiamo inviare azioni dall'interno di un componente di reazione. Redux è sicuramente indipendente dalla reazione.
Tuananhcwrs

18

Se stai usando React Hooks, una soluzione a riga singola è

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

L'array vuoto garantisce che venga chiamato solo una volta, al primo rendering.

Esempio completo:

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

11

Aggiornamento 2020 : insieme ad altre soluzioni, utilizzo il middleware Redux per controllare ogni richiesta di tentativi di accesso non riusciti:

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

Aggiornamento 2018 : questa risposta è per React Router 3

Ho risolto questo problema usando React -router onEnter props. Ecco come appare il codice:

// 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
Giusto per essere chiari, react-router 4 non supporta onEnter.
Rob L

L'IntlProvider dovrebbe darti un suggerimento per una soluzione migliore .. Vedi la mia risposta sotto.
Chris Kemp

questo usa il vecchio React-Router v3, guarda la mia risposta
stackdave

3

Con il middleware redux-saga puoi farlo bene.

Basta definire una saga che non sta guardando l'azione inviata (ad esempio con takeo takeLatest) prima di essere attivata. Quando viene modificato forkdalla saga di root in questo modo, verrà eseguito esattamente una volta all'avvio dell'app.

Quello che segue è un esempio incompleto che richiede un po 'di conoscenza del redux-sagapacchetto ma illustra il punto:

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

Il codice sopra invia un'azione launchStarte launchCompleteredux che dovresti creare. È buona norma creare tali azioni in quanto tornano utili per notificare allo stato di fare altre cose ogni volta che il lancio è iniziato o completato.

La tua saga di root dovrebbe quindi forkare questa launchSagasaga:

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

Si prega di leggere l'ottima documentazione di redux-saga per maggiori informazioni a riguardo.


la pagina non verrà caricata fino al completamento di questa azione corretta?
Markov

2

Ecco una risposta utilizzando l'ultima versione di 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>);
}

L'app deve essere in Provider. Per rendere felice TypeScript, ho dovuto aggiungere una chiusura extra attorno alla spedizione: useEffect (() => {dispatch (AppInit ())}, []).
PEZO

0

Stavo usando redux-thunk per recuperare gli account sotto un utente da un endpoint API su inizializzazione dell'app, ed era asincrono quindi i dati arrivavano dopo il rendering della mia app e la maggior parte delle soluzioni sopra non ha fatto miracoli per me e alcune lo sono ammortizzato. Così ho guardato a componentDidUpdate (). Quindi fondamentalmente su APP init dovevo avere elenchi di account dall'API e gli account del mio negozio Redux sarebbero nulli o []. Ricorso a questo dopo.

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

Se stai usando React Hooks, puoi semplicemente inviare un'azione usando React.useEffect

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

Uso questo modello per l' onAuthStateChangedascoltatore di registro

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

Utilizzo: Apollo Client 2.0, React-Router v4, React 16 (fibra)

La risposta selezionata utilizza il vecchio React Router v3. Avevo bisogno di "invio" per caricare le impostazioni globali per l'app. Il trucco sta usando componentWillUpdate, sebbene l'esempio utilizzi il client apollo, e non recuperare le soluzioni è equivalente. Non hai bisogno di boucle di

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
Questo codice è incompleto e deve essere ritagliato per le parti irrilevanti della domanda.
Naoise Golden,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.