React / Redux - akcja wysyłania przy ładowaniu / inicjowaniu aplikacji

85

Mam uwierzytelnianie tokenem z serwera, więc kiedy moja aplikacja Redux jest ładowana początkowo, muszę wysłać żądanie do tego serwera, aby sprawdzić, czy użytkownik jest uwierzytelniony, czy nie, a jeśli tak, powinienem otrzymać token.

Zauważyłem, że używanie podstawowych akcji INIT Redux nie jest zalecane, więc jak mogę wywołać akcję, zanim aplikacja zostanie wyrenderowana?

Shota
źródło

Odpowiedzi:

77

Możesz wywołać akcję w componentDidMountmetodzie Root , aw rendermetodzie możesz zweryfikować status autoryzacji.

Coś takiego:

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
źródło
1
Dla mnie to componentWillMount()zrobił. Zdefiniowałem prostą funkcję wywołującą wszystkie działania związane mapDispatchToProps()z wysyłką w App.js i wywołałem ją componentWillMount().
Froxx,
To świetnie, ale użycie mapDispatchToProps wydaje się bardziej opisowe. Jakie jest Twoje uzasadnienie za używaniem mapStateToProps zamiast tego?
tcelferact
@ adc17 Ups :) dzięki za komentarz. Zmieniłem odpowiedź!
Serhii Baraniuk
@SerhiiBaraniuk bez obaw. Jeszcze jedno: zakładając, że getAuthjest twórcą akcji, myślę, że chciałbyś zdefiniować dispatchjako parametr mapDispatchToProps, tj., const mapDispatchToProps = dispatch => {A następnie zrobić coś takiego:getAuth: () => { dispatch(getAuth()); }
tcelferact
2
Otrzymałem ten błąd, próbując wdrożyć to rozwiązanieUncaught 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

Nie podobały mi się żadne zaproponowane rozwiązania, a potem przyszło mi do głowy, że myślę o zajęciach do wyrenderowania. A co jeśli właśnie utworzyłem klasę do uruchamiania, a następnie wrzuciłem elementy do componentDidMountmetody i po prostu renderwyświetlił się ekran ładowania?

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

A potem coś takiego:

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

Następnie napisz kilka akcji redux, aby asynchronicznie zainicjować aplikację. Działa wspaniale.

Chris Kemp
źródło
Teraz to jest rozwiązanie, którego szukałem! Wierzę, że twój wgląd tutaj jest całkowicie właściwy. Dzięki.
YanivGK,
26

Wszystkie odpowiedzi tutaj wydają się być wariacjami na temat tworzenia komponentu root i odpalania go w module componentDidMount. Jedną z rzeczy, które najbardziej podoba mi się w Redux, jest to, że oddziela pobieranie danych od cykli życia komponentów. Nie widzę powodu, dla którego miałoby być inaczej w tym przypadku.

Jeśli importujesz swój sklep do index.jspliku głównego , możesz po prostu wysłać swojego kreatora akcji (nazwijmy to initScript()) do tego pliku i uruchomi się, zanim cokolwiek zostanie załadowane.

Na przykład:

//index.js

store.dispatch(initScript());

ReactDOM.render(
  <Provider store={store}>
    <Routes />
  </Provider>,
  document.getElementById('root')
);
Josh Pittman
źródło
1
Jestem nowicjuszem, ale po przeczytaniu wstępnej dokumentacji dotyczącej koncepcji reagowania i reduxu uważam, że jest to najbardziej odpowiedni sposób. czy jest jakaś korzyść z tworzenia tych inicjalizacji podczas componentDidMountwydarzenia?
kuzditomi
1
To naprawdę zależy od sytuacji. Więc componentDidMountbędzie odpalić przed zamontowaniem określonego komponentu. Wypalanie store.dispatch()przed ReactDOM.render () `pożary przed wierzchowców aplikacji. To trochę jak componentWillMountdla całej aplikacji. Jako nowicjusz uważam, że lepiej jest trzymać się metod cyklu życia komponentów, ponieważ zapewnia to ścisłe powiązanie logiki z miejscem jej używania. Ponieważ aplikacje stają się coraz bardziej złożone, staje się to trudniejsze do kontynuowania. Radziłbym zachować prostotę tak długo, jak możesz.
Josh Pittman
1
Ostatnio musiałem zastosować powyższe podejście. Miałem przycisk logowania do Google i musiałem uruchomić skrypt, aby działał przed załadowaniem aplikacji. Gdybym czekał, aż aplikacja się załaduje, a następnie zadzwoni, otrzymanie odpowiedzi zajmie więcej czasu i opóźni działanie aplikacji. Jeśli robienie rzeczy w cyklu życia działa w twoim przypadku użycia, trzymaj się cykli życia. Łatwiej o nich myśleć. Dobrym sposobem oceny tego jest wyobrażenie sobie, jak patrzysz na kod za 6 miesięcy. Które podejście byłoby łatwiejsze do intuicyjnego zrozumienia. Wybierz takie podejście.
Josh Pittman
Witaj @JoshPittman, nadal musisz podłączyć komponent root, np. "App" lub coś podobnego, aby zapisać się na aktualizację stanu redux. Jest to więc tak, jakbyś unikał wysyłania akcji tylko z metody componentDidMount ().
Tuananhcwrs
1
Mówię TAK w kwestii wysyłania. Redux nie mówi, że musimy wysyłać akcje z wnętrza komponentu reagującego. Redux jest z pewnością niezależny od reagowania.
Tuananhcwrs
16

Jeśli używasz React hooków, jednym rozwiązaniem byłoby:

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

Pusta tablica zapewni, że zostanie wywołana tylko raz, przy pierwszym renderowaniu.

Pełny przykład:

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
źródło
11

Aktualizacja 2020 : Oprócz innych rozwiązań używam oprogramowania pośredniego Redux do sprawdzania każdego żądania pod kątem nieudanych prób logowania:

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

Aktualizacja 2018 : Ta odpowiedź dotyczy React Router 3

Rozwiązałem ten problem za pomocą właściwości React -router onEnter . Tak wygląda kod:

// 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
źródło
11
Dla jasności, react-router 4 nie obsługuje onEnter.
Rob L
IntlProvider powinien dać ci wskazówkę co do lepszego rozwiązania. Zobacz moją odpowiedź poniżej.
Chris Kemp,
to używa starego routera reagującego v3, spójrz na moją odpowiedź
stackdave
3

Dzięki oprogramowaniu pośredniczącemu redux-saga możesz to zrobić przyjemnie.

Po prostu zdefiniuj sagę, która nie oczekuje wysłania akcji (np. Z takelub takeLatest) przed uruchomieniem. Po forkwydaniu z głównej sagi w ten sposób uruchomi się dokładnie raz podczas uruchamiania aplikacji.

Poniższy przykład jest niekompletny, który wymaga trochę wiedzy na temat redux-sagapakietu, ale ilustruje punkt:

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

Powyższy kod wywołuje akcję a launchStarti launchCompleteredux, którą należy utworzyć. Dobrą praktyką jest tworzenie takich akcji, ponieważ przydają się one do powiadamiania państwa o innych rzeczach, gdy start lub koniec uruchomienia się rozpoczął.

Twoja saga root powinna następnie rozwidlić tę launchSagasagę:

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

Przeczytaj naprawdę dobrą dokumentację sagi redux, aby uzyskać więcej informacji na jej temat.

Andru
źródło
strona nie zostanie załadowana, dopóki ta czynność nie zostanie zakończona, prawda?
Markov
1

Oto odpowiedź wykorzystująca najnowszą wersję React (16.8), hooki:

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
źródło
0

Używałem redux-thunk do pobierania kont użytkownika z punktu końcowego API podczas inicjowania aplikacji i było to asynchroniczne, więc dane przychodziły po wyrenderowaniu mojej aplikacji i większość powyższych rozwiązań nie zdziała dla mnie cudów, a niektóre są amortyzowane. Więc spojrzałem na componentDidUpdate (). Więc w zasadzie w APP init musiałem mieć listy kont z API, a moje konta w sklepie redux miałyby wartość null lub []. Uciekłem się do tego później.

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
źródło
0

Jeśli używasz React Hooks, możesz po prostu wywołać akcję za pomocą React.useEffect

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

Używam tego wzorca dla onAuthStateChangedsłuchacza rejestru

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
źródło
-1

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

Wybrana odpowiedź używa starego React Router v3. Musiałem wykonać „wysyłkę”, aby załadować globalne ustawienia aplikacji. Sztuczka polega na użyciu componentWillUpdate, chociaż przykład używa klienta apollo, a nie pobieranie rozwiązań jest równoważne. Nie potrzebujesz boucle

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
źródło
2
Ten kod jest niekompletny i wymaga przycięcia dla nieistotnych części pytania.
Naoise Golden