React / Redux: acción de despacho en la carga / inicio de la aplicación

85

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?

Shota
fuente

Respuestas:

77

Puede enviar una acción en el componentDidMountmétodo Root y en el rendermétodo puede verificar el estado de autenticación.

Algo como esto:

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
fuente
1
Para mí 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().
Froxx
Esto es genial, pero usar mapDispatchToProps parece más descriptivo. ¿Cuál es su razón de ser para usar mapStateToProps en su lugar?
tcelferact
@ adc17 Oooops :) gracias por el comentario. ¡He cambiado mi respuesta!
Serhii Baraniuk
@SerhiiBaraniuk no te preocupes. Una cosa más: suponiendo que getAuthes un creador de acción, creo que querrás definir dispatchcomo un parámetro de mapDispatchToProps, es decir, const mapDispatchToProps = dispatch => {y luego hacer algo como:getAuth: () => { dispatch(getAuth()); }
tcelferact
2
Recibí este error al intentar implementar esta soluciónUncaught 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

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 componentDidMountmétodo y solo tengo la renderpantalla 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.

Chris Kemp
fuente
¡Esa es la solución que he estado buscando! Creo que su percepción aquí es perfectamente correcta. Gracias.
YanivGK
26

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.jsarchivo raíz , puede enviar su creador de acciones (llamémoslo initScript()) 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')
);
Josh Pittman
fuente
1
Soy un novato en react, pero según la lectura de la documentación inicial sobre los conceptos de react y redux, creo que esta es la forma más adecuada. ¿Existe alguna ventaja para crear estas inicializaciones en un componentDidMountevento?
kuzditomi
1
Realmente depende de la situación. Entonces, componentDidMountse disparará antes de que se monte un componente específico. Disparar store.dispatch()antes de ReactDOM.render () `se dispara antes de que se monte la aplicación. Es algo así como componentWillMountpara 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.
Josh Pittman
1
Tuve que usar el enfoque anterior recientemente. Tenía un botón de inicio de sesión de Google y necesitaba activar un script para que funcionara antes de que se cargara la aplicación. Si hubiera esperado a que se cargara la aplicación y luego hiciera la llamada, habría tardado más en obtener la respuesta y habría retrasado la funcionalidad de la aplicación. Si hacer cosas en un ciclo de vida funciona para su caso de uso, apéguese a los ciclos de vida. Son más simples de pensar. Una buena forma de juzgar esto es imaginarse a sí mismo mirando el código dentro de seis meses. Qué enfoque le resultaría más fácil de entender intuitivamente. Elija ese enfoque.
Josh Pittman
Hola @JoshPittman, todavía necesitas conectar el componente raíz, por ejemplo, la "Aplicación" o algo similar para suscribirte a la actualización del estado de redux. Así que es como si evitara enviar acciones desde su método componentDidMount () solamente.
Tuananhcwrs
1
Digo SÍ a su punto sobre el envío. Redux no dice que tengamos que enviar acciones desde dentro de un componente de reacción. Seguramente Redux es independiente de react.
Tuananhcwrs
16

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

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>
);
Shota
fuente
11
Para que quede claro, react-router 4 no es compatible con onEnter.
Rob L
IntlProvider debería darle una pista sobre una mejor solución. Vea mi respuesta a continuación.
Chris Kemp
esto usa el viejo react-router v3, mira mi respuesta
stackdave
3

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 takeo takeLatest) antes de activarse. Cuando se forkedita 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-sagapaquete, 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 launchStarty launchCompleteredux 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 launchSagasaga:

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.

Andru
fuente
la página no se cargará hasta que se complete esta acción, ¿correcto?
Markov
1

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>);
}
Eric Blade
fuente
0

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

Si está usando React Hooks, simplemente puede enviar una acción usando React.useEffect

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

Utilizo este patrón para registrar el onAuthStateChangedoyente

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
fuente
-1

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"
              />
apilado
fuente
2
Este código está incompleto y debe recortarse para las partes irrelevantes de la pregunta.
Naoise Golden