Tengo autenticación de token de un servidor, por lo que cuando mi aplicación Redux se carga inicialmente, necesito hacer una solicitud a este servidor para verificar si el usuario está autenticado o no, y si es así, debería obtener el token.
Descubrí que no se recomienda el uso de acciones INIT del núcleo de Redux, entonces, ¿cómo puedo enviar una acción antes de que se procese la aplicación?
componentWillMount()
hizo la cosa.mapDispatchToProps()
Definí una función simple que llama a todas las acciones relacionadas con el envío en App.js y la llamécomponentWillMount()
.getAuth
es un creador de acción, creo que querrás definirdispatch
como un parámetro demapDispatchToProps
, es decir,const mapDispatchToProps = dispatch => {
y luego hacer algo como: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)".
No he estado contento con ninguna solución que se haya presentado para esto, y luego se me ocurrió que estaba pensando en las clases que debían renderizarse. ¿Qué pasa si acabo de crear una clase para el inicio y luego introduzco cosas en el
componentDidMount
método y solo tengo larender
pantalla de carga?<Provider store={store}> <Startup> <Router> <Switch> <Route exact path='/' component={Homepage} /> </Switch> </Router> </Startup> </Provider>
Y luego tenga algo como esto:
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);
Luego, escriba algunas acciones de redux para inicializar asíncronamente su aplicación. Funciona de maravilla.
fuente
Todas las respuestas aquí parecen ser variaciones sobre la creación de un componente raíz y su activación en el componentDidMount. Una de las cosas que más disfruto de redux es que desacopla la obtención de datos de los ciclos de vida de los componentes. No veo ninguna razón por la que deba ser diferente en este caso.
Si está importando su tienda en el
index.js
archivo raíz , puede enviar su creador de acciones (llamémosloinitScript()
) en ese archivo y se activará antes de que se cargue algo.Por ejemplo:
//index.js store.dispatch(initScript()); ReactDOM.render( <Provider store={store}> <Routes /> </Provider>, document.getElementById('root') );
fuente
componentDidMount
evento?componentDidMount
se disparará antes de que se monte un componente específico. Dispararstore.dispatch()
antes de ReactDOM.render () `se dispara antes de que se monte la aplicación. Es algo así comocomponentWillMount
para toda la aplicación. Como novato, creo que es mejor ceñirse al uso de los métodos del ciclo de vida de los componentes porque mantiene la lógica estrechamente acoplada al lugar donde se está utilizando. A medida que las aplicaciones se vuelven cada vez más complejas, esto se vuelve más difícil de seguir. Mi consejo sería mantenerlo simple durante el mayor tiempo posible.Si está utilizando React Hooks, una solución de una sola línea sería:
useEffect(() => store.dispatch(handleAppInit()), []);
La matriz vacía garantizaría que se llame solo una vez, en el primer renderizado.
Ejemplo completo:
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;
fuente
Actualización 2020 : junto con otras soluciones, estoy usando el middleware Redux para verificar cada solicitud en busca de intentos fallidos de inicio de sesión:
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; };
Actualización 2018 : esta respuesta es para React Router 3
Resolví este problema usando react-router onEnter props. Así es como se ve el código:
// 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> );
fuente
Con el middleware redux-saga puedes hacerlo muy bien.
Simplemente defina una saga que no esté pendiente de la acción enviada (por ejemplo, con
take
otakeLatest
) antes de activarse. Cuando sefork
edita desde la saga raíz de esa manera, se ejecutará exactamente una vez al inicio de la aplicación.El siguiente es un ejemplo incompleto que requiere un poco de conocimiento sobre el
redux-saga
paquete, pero ilustra el punto: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];
El código anterior envía una acción
launchStart
ylaunchComplete
redux que debe crear. Es una buena práctica crear este tipo de acciones, ya que resultan útiles para notificar al estado que haga otras cosas cada vez que se inicia o completa el lanzamiento.Tu saga raíz debería bifurcar esta
launchSaga
saga: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;
Lea la muy buena documentación de redux-saga para obtener más información al respecto.
fuente
Aquí hay una respuesta usando lo último en 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>); }
fuente
Estaba usando redux-thunk para obtener cuentas de un usuario desde un punto final de API en el inicio de la aplicación, y era asíncrono, por lo que los datos entraban después de que mi aplicación se procesó y la mayoría de las soluciones anteriores no me hicieron maravillas y algunas son depreciado. Así que miré a componentDidUpdate (). Básicamente, en el inicio de la aplicación, tenía que tener listas de cuentas de API, y mis cuentas de la tienda redux serían nulas o []. Recurrió a esto después.
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);
fuente
Si está usando React Hooks, simplemente puede enviar una acción usando React.useEffect
Utilizo este patrón para registrar el
onAuthStateChanged
oyentefunction 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);
fuente
Utilizando: Apollo Client 2.0, React-Router v4, React 16 (fibra)
La respuesta seleccionada usa el antiguo React Router v3. Necesitaba hacer 'despacho' para cargar la configuración global de la aplicación. El truco es usar componentWillUpdate, aunque el ejemplo usa el cliente apollo, y no buscar las soluciones es equivalente. No necesitas boucle de
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" />
fuente