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?
componentWillMount()
zrobił. Zdefiniowałem prostą funkcję wywołującą wszystkie działania związanemapDispatchToProps()
z wysyłką w App.js i wywołałem jącomponentWillMount()
.getAuth
jest twórcą akcji, myślę, że chciałbyś zdefiniowaćdispatch
jako parametrmapDispatchToProps
, tj.,const mapDispatchToProps = dispatch => {
A następnie zrobić coś takiego:getAuth: () => { dispatch(getAuth()); }
Uncaught 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)".
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
componentDidMount
metody i po prosturender
wyś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.
źródło
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.js
pliku głównego , możesz po prostu wysłać swojego kreatora akcji (nazwijmy toinitScript()
) 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') );
źródło
componentDidMount
wydarzenia?componentDidMount
będzie odpalić przed zamontowaniem określonego komponentu. Wypalaniestore.dispatch()
przed ReactDOM.render () `pożary przed wierzchowców aplikacji. To trochę jakcomponentWillMount
dla 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.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;
źródło
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> );
źródło
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
take
lubtakeLatest
) przed uruchomieniem. Pofork
wydaniu 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-saga
pakietu, 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
launchStart
ilaunchComplete
redux, 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ę
launchSaga
sagę: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.
źródło
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>); }
źródło
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);
źródło
Jeśli używasz React Hooks, możesz po prostu wywołać akcję za pomocą React.useEffect
Używam tego wzorca dla
onAuthStateChanged
słuchacza rejestrufunction 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);
źródło
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" />
źródło