React / Redux - действие отправки при загрузке / инициализации приложения

88

У меня есть аутентификация токена с сервера, поэтому, когда мое приложение Redux загружается изначально, мне нужно сделать запрос на этот сервер, чтобы проверить, аутентифицирован ли пользователь или нет, и если да, я должен получить токен.

Я обнаружил, что использование действий INIT ядра Redux не рекомендуется, поэтому как я могу отправить действие до того, как приложение будет отрисовано?

Шота
источник

Ответы:

78

Вы можете отправить действие в componentDidMountметоде Root, а в renderметоде вы можете проверить статус аутентификации.

Что-то вроде этого:

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)
Сергей Баранюк
источник
1
Для меня это было componentWillMount()сделано. Я определил простую функцию, вызывающую все действия, связанные с отправкой, в mapDispatchToProps()App.js и вызвал ее componentWillMount().
Froxx
Это здорово, но использование mapDispatchToProps кажется более наглядным. Каково ваше обоснование использования вместо этого mapStateToProps?
tcelferact
@ adc17 Оооо :) спасибо за комментарий. Я изменил свой ответ!
Сергей Баранюк
1
@ adc17 цитата из документа :[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.
Сергей Баранюк,
2
Я получил эту ошибку при попытке реализовать это решение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)".
markhops
35

Я не был доволен какими-либо решениями, которые были предложены для этого, и тогда мне пришло в голову, что я думал о классах, которые необходимо отрендерить. Как насчет того, чтобы я просто создал класс для запуска, а затем вставил все в componentDidMountметод и просто renderотобразил экран загрузки?

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

А потом получится что-то вроде этого:

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

Затем напишите несколько действий redux для асинхронной инициализации вашего приложения. Работает удовольствие.

Крис Кемп
источник
Вот решение, которое я искал! Я считаю, что ваше понимание здесь совершенно верно. Благодарю.
YanivGK
29

Все ответы здесь кажутся вариантами создания корневого компонента и его запуска в componentDidMount. Одна из вещей, которые мне больше всего нравятся в redux, - это то, что он отделяет получение данных от жизненного цикла компонентов. Я не вижу причин, почему в данном случае должно быть иначе.

Если вы импортируете свой магазин в корневой index.jsфайл, вы можете просто отправить создателя действий (назовем его initScript()) в этот файл, и он сработает до того, как что-нибудь загрузится.

Например:

//index.js

store.dispatch(initScript());

ReactDOM.render(
  <Provider store={store}>
    <Routes />
  </Provider>,
  document.getElementById('root')
);
Джош Питтман
источник
1
Я новичок в реакции, но, основываясь на чтении исходной документации по концепциям реакции и сокращения, я считаю, что это наиболее подходящий способ. есть ли какие-либо преимущества в создании этих инициализаций для componentDidMountсобытия?
kuzditomi
1
Это действительно зависит от ситуации. Таким образом, componentDidMountсработает перед установкой определенного компонента. Увольнение store.dispatch()до того ReactDOM.render () `пожары до того , как приложение монтирует. Это вроде как componentWillMountдля всего приложения. Как новичок, я думаю, что лучше придерживаться методов жизненного цикла компонентов, поскольку они тесно связаны с логикой того, где они используются. По мере того, как приложения становятся все более сложными, делать это становится все труднее. Я бы посоветовал сделать это простым как можно дольше.
Джош Питтман
1
Недавно мне пришлось использовать описанный выше подход. У меня была кнопка входа в Google, и мне нужно было запустить скрипт, чтобы он работал, прежде чем приложение загрузится. Если бы я дождался загрузки приложения, а затем позвонил бы, мне потребовалось бы больше времени, чтобы получить ответ и отложить функциональность в приложении. Если выполнение действий в жизненном цикле работает для вашего варианта использования, придерживайтесь жизненных циклов. О них проще думать. Хороший способ судить об этом - представить, как вы смотрите на код через 6 месяцев. Какой подход вам будет легче интуитивно понять. Выберите этот подход.
Джош Питтман
1
Кроме того, вам действительно не нужно подписываться на обновления на redux, просто нужно отправить. В этом весь смысл этого подхода, я пользуюсь преимуществом того факта, что redux отделяет выполнение задачи (выборка данных, запуск действия и т. Д.) И использование результата (рендеринг, ответ и т. Д.).
Джош Питтман
2
Я говорю ДА на ваш вопрос об отправке. Redux не говорит, что мы должны отправлять действия из компонента реакции. Redux определенно не зависит от реакции.
Tuananhcwrs
18

Если вы используете React Hooks, одно однострочное решение

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

Пустой массив гарантирует, что он вызывается только один раз при первом рендеринге.

Полный пример:

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>
  );
}
Матиас Бервиг
источник
11

Обновление 2020 : наряду с другими решениями я использую промежуточное ПО Redux для проверки каждого запроса на наличие неудачных попыток входа в систему:

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

Обновление 2018 : это ответ для React Router 3

Я решил эту проблему, используя response -router onEnter props . Вот как выглядит код:

// 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>
);
Шота
источник
11
Для ясности, response-router 4 не поддерживает onEnter.
Роб Л.
IntlProvider должен подсказать вам лучшее решение .. См. Мой ответ ниже.
Крис Кемп
здесь используется старый
реактивный
3

С промежуточным программным обеспечением redux-saga вы можете сделать это хорошо.

Просто определите сагу, которая не отслеживает отправленное действие (например, с помощью takeили takeLatest) перед запуском. При forkтаком запуске из корневой саги он будет запускаться ровно один раз при запуске приложения.

Ниже приводится неполный пример, который требует некоторых знаний о redux-sagaпакете, но иллюстрирует суть:

саги / 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];

Приведенный выше код отправляет launchStartиlaunchComplete redux, которое вы должны создать. Рекомендуется создавать такие действия, поскольку они пригодятся, чтобы уведомить государство о выполнении других действий при запуске или завершении запуска.

Затем ваша корневая сага должна разветвляться на эту launchSagaсагу:

саги / 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;

Пожалуйста, прочтите действительно хорошую документацию по redux-saga для получения дополнительной информации.

Андру
источник
страница не загружается, пока это действие не будет выполнено правильно?
Марков
2

Вот ответ с использованием последней версии 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>);
}
Эрик Блейд
источник
Приложение должно быть от провайдера. Чтобы сделать TypeScript счастливым, мне пришлось добавить дополнительное закрытие для отправки: useEffect (() => {dispatch (AppInit ())}, []).
PEZO
0

Я использовал redux-thunk для извлечения учетных записей пользователя из конечной точки API при инициализации приложения, и это было асинхронно, поэтому данные поступали после рендеринга моего приложения, и большинство вышеперечисленных решений не творили для меня чудес, а некоторые из них обесценивается. Поэтому я посмотрел на componentDidUpdate (). Таким образом, в основном при инициализации приложения мне приходилось иметь списки учетных записей из API, и мои учетные записи в хранилище redux были бы нулевыми или []. К этому прибегли уже после.

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);
Маршалл Фунгай
источник
0

Если вы используете React Hooks, вы можете просто отправить действие с помощью React.useEffect

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

Я использую этот шаблон для onAuthStateChangedслушателя регистра

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
источник
-1

Использование: Apollo Client 2.0, React-Router v4, React 16 (Fiber).

В выбранном ответе используется старый React Router v3. Мне нужно было выполнить «отправку», чтобы загрузить глобальные настройки приложения. Хитрость заключается в использовании componentWillUpdate, хотя в примере используется клиент apollo, а не получение решений эквивалентно. Вам не нужна букле

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"
              />
стеклопакет
источник
2
Этот код неполный и нуждается в обрезке, чтобы удалить части, не относящиеся к вопросу.
Naoise Golden