React / Redux - ação de despacho na carga / inicialização do aplicativo

88

Eu tenho autenticação por token de um servidor, então quando meu aplicativo Redux é carregado inicialmente eu preciso fazer uma solicitação a este servidor para verificar se o usuário está autenticado ou não, e se sim, eu devo obter o token.

Descobri que o uso de ações básicas INIT do Redux não é recomendado, então como posso despachar uma ação, antes que o aplicativo seja renderizado?

Shota
fonte

Respostas:

78

Você pode despachar uma ação no componentDidMountmétodo Root e no rendermétodo você pode verificar o status de autenticação.

Algo assim:

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
fonte
1
Para mim componentWillMount()fez a coisa. Eu defini uma função simples chamando todas as ações relacionadas ao despacho mapDispatchToProps()do App.js e a chamei componentWillMount().
Froxx
Isso é ótimo, mas usar mapDispatchToProps parece mais descritivo. Qual é a sua lógica por trás do uso de mapStateToProps?
tcelferact
@ adc17 Oooops :) obrigado pelo comentário. Eu mudei minha resposta!
Serhii Baraniuk
1
@ adc17 citação do 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
Recebi este erro ao tentar implementar esta soluçãoUncaught 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 de
35

Não fiquei satisfeito com as soluções propostas para isso, e então me ocorreu que estava pensando nas aulas que precisavam ser renderizadas. E se eu apenas criasse uma classe para inicialização e empurre as coisas para o componentDidMountmétodo e apenas renderexibisse uma tela de carregamento?

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

E então tem algo assim:

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

Em seguida, escreva algumas ações redux para inicializar de forma assíncrona seu aplicativo. Funciona muito bem.

Chris Kemp
fonte
Essa é a solução que estou procurando! Eu acredito que sua visão aqui está perfeitamente certa. Obrigado.
YanivGK
29

Todas as respostas aqui parecem ser variações sobre a criação de um componente raiz e acioná-lo no componentDidMount. Uma das coisas que mais gosto no redux é que ele separa a busca de dados dos ciclos de vida do componente. Não vejo razão para que seja diferente neste caso.

Se você estiver importando sua loja para o index.jsarquivo raiz , você pode simplesmente despachar seu criador de ação (vamos chamá-lo initScript()) nesse arquivo e ele irá disparar antes que qualquer coisa seja carregada.

Por exemplo:

//index.js

store.dispatch(initScript());

ReactDOM.render(
  <Provider store={store}>
    <Routes />
  </Provider>,
  document.getElementById('root')
);
Josh Pittman
fonte
1
Sou um novato no react, mas com base na leitura da documentação inicial sobre os conceitos de react e redux, acredito que esta é a forma mais adequada. há alguma vantagem em criar essas inicializações em um componentDidMountevento?
kuzditomi
1
Isso depende da situação. Portanto, o componentDidMountserá disparado antes da montagem de um componente específico. Disparando store.dispatch()antes de ReactDOM.render () `disparar antes da montagem do aplicativo. É como um componentWillMountpara todo o aplicativo. Como um novato, acho que é melhor continuar usando os métodos de ciclo de vida do componente porque mantém a lógica fortemente acoplada ao local onde está sendo usado. Conforme os aplicativos ficam cada vez mais complexos, isso se torna mais difícil de continuar. Meu conselho seria mantê-lo simples pelo maior tempo possível.
Josh Pittman de
1
Tive que usar a abordagem acima recentemente. Eu tinha um botão de login do Google e precisava disparar um script para fazê-lo funcionar antes que o aplicativo fosse carregado. Se eu esperasse o aplicativo carregar e depois fizesse a chamada, demoraria mais para obter a resposta e atrasaria a funcionalidade do aplicativo. Se fazer as coisas em um ciclo de vida funciona para o seu caso de uso, mantenha-se nos ciclos de vida. Eles são mais simples de pensar. Uma boa maneira de julgar isso é se imaginar olhando para o código daqui a 6 meses. Qual abordagem seria mais fácil de entender intuitivamente. Escolha essa abordagem.
Josh Pittman
1
Além disso, você realmente não precisa se inscrever para receber a atualização no redux, apenas precisa despachar. Esse é o objetivo dessa abordagem, estou aproveitando o fato de que o redux desacopla a execução da tarefa (obtenção de dados, acionamento de uma ação etc.) e o uso do resultado (renderização, resposta etc.).
Josh Pittman de
2
Eu digo SIM ao seu ponto sobre despacho. Redux não diz que temos que despachar ações de dentro de um componente de reação. Redux é certamente independente de reação.
Tuananhcwrs de
18

Se você estiver usando React Hooks, uma solução de linha única é

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

O array vazio garante que ele seja chamado apenas uma vez, na primeira renderização.

Exemplo 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>
  );
}
Mathias Berwig
fonte
11

Atualização 2020 : junto com outras soluções, estou usando o middleware Redux para verificar cada solicitação de tentativas de login malsucedidas:

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

Atualização 2018 : Esta resposta é para React Router 3

Resolvi esse problema usando os adereços react -router onEnter . Esta é a aparência do 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
fonte
11
Só para ficar claro, o react-router 4 não suporta onEnter.
Rob L
O IntlProvider deve lhe dar uma dica de uma solução melhor. Veja minha resposta abaixo.
Chris Kemp
isso usa o velho
react
3

Com o middleware redux-saga, você pode fazer isso muito bem.

Basta definir uma saga que não esteja esperando por ação despachada (por exemplo, com takeou takeLatest) antes de ser ativada. Quando forkretirado da saga raiz assim, ele será executado exatamente uma vez na inicialização do aplicativo.

O seguinte é um exemplo incompleto que requer um pouco de conhecimento sobre o redux-sagapacote, mas ilustra o ponto:

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

O código acima despacha uma ação launchStarte launchCompleteredux que você deve criar. É uma boa prática criar essas ações, pois elas são úteis para notificar o estado para fazer outras coisas sempre que o lançamento for iniciado ou concluído.

Sua saga raiz deve então ramificar 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;

Por favor, leia a documentação realmente boa do redux-saga para mais informações sobre ele.

Andru
fonte
a página não carrega até que esta ação seja concluída, correto?
Markov
2

Aqui está uma resposta usando o mais recente em 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
fonte
O aplicativo deve estar em Provedor. Para deixar o TypeScript feliz, tive que adicionar um fechamento extra em torno do despacho: useEffect (() => {dispatch (AppInit ())}, []).
PEZO
0

Eu estava usando redux-thunk para buscar contas em um usuário de um ponto de extremidade de API no aplicativo init e era assíncrono, então os dados estavam chegando após meu aplicativo renderizado e a maioria das soluções acima não fizeram maravilhas para mim e algumas são depreciado. Então eu olhei para componentDidUpdate (). Então, basicamente, no init do APP, eu precisava ter listas de contas da API e minhas contas de loja redux seriam nulas ou []. Recorri a isso depois.

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
fonte
0

Se estiver usando React Hooks, você pode simplesmente despachar uma ação usando React.useEffect

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

Eu uso este padrão para registrar onAuthStateChangedouvinte

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

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

A resposta selecionada usa o antigo React Router v3. Eu precisava fazer 'despacho' para carregar as configurações globais do aplicativo. O truque é usar o componentWillUpdate, embora o exemplo esteja usando o cliente Apollo, e não buscar as soluções é equivalente. Você não precisa de 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"
              />
Stackdave
fonte
2
Este código está incompleto e precisa ser corrigido para as partes irrelevantes da questão.
Naoise Golden