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?
componentWillMount()
fez a coisa. Eu defini uma função simples chamando todas as ações relacionadas ao despachomapDispatchToProps()
do App.js e a chameicomponentWillMount()
.[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.
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)".
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
componentDidMount
método e apenasrender
exibisse 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.
fonte
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.js
arquivo raiz , você pode simplesmente despachar seu criador de ação (vamos chamá-loinitScript()
) 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') );
fonte
componentDidMount
evento?componentDidMount
será disparado antes da montagem de um componente específico. Disparandostore.dispatch()
antes de ReactDOM.render () `disparar antes da montagem do aplicativo. É como umcomponentWillMount
para 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.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> ); }
fonte
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> );
fonte
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
take
outakeLatest
) antes de ser ativada. Quandofork
retirado 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-saga
pacote, 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
launchStart
elaunchComplete
redux 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
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;
Por favor, leia a documentação realmente boa do redux-saga para mais informações sobre ele.
fonte
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>); }
fonte
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);
fonte
Se estiver usando React Hooks, você pode simplesmente despachar uma ação usando React.useEffect
Eu uso este padrão para registrar
onAuthStateChanged
ouvintefunction 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);
fonte
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" />
fonte