Подтвердить что ты не робот

Асинхронный поток данных в приложении React с Redux + ReactRouter?

Я использую Redux и React-router, чтобы создать простую почтовую программу. Поскольку я довольно новичок в Redux, я не совсем понимаю фактический поток данных в Redux + Router.

Что я пытаюсь получить

  • После запуска страницы (/) MailListComponent выбирает массив сообщений с сервера. В это время MessageComponent не отображается, так как у него нет единого сообщения для извлечения данных для него.
  • После выбора state.messages:[] приложение переходит к первому сообщению state.messages:[] (/messages/1) `.
  • После завершения перехода отображается MessageComponent и выводит сообщение с идентификатором id = 1, а в отдельном запросе - вложения.

Здесь модель компонента:

Компонентная модель

Что я делаю

// MailListActions.js
export function loadMessages() {
  return {
    type:    'LOAD_MESSAGES',
    promise: client => client.get('/messages')
  };
}

// MailListReducer.js
import Immutable from 'immutable';

const defaultState = { messages: [], fetchingMessages: false };

export default function mailListReducer(state = defaultState, action = {}) {
  switch (action.type) {
    case 'LOAD_MESSAGES_REQUEST':
      return state.merge({fetchingMessages: true});

    case 'LOAD_MESSAGES':
        return state.merge({fetchingMessages: false, messages: action.res.data || null});

    case 'LOAD_MESSAGES_FAILURE':
        // also do something

    default:
      return state;
  }
}

Поскольку я использую promiseMiddleware, LOAD_MESSAGES, LOAD_MESSAGES_REQUEST и LOAD_MESSAGES_FAILURE отправляются по запросу /messages.

И теперь:

  • Можно ли отправить loadMessages() в componentDidMount из MailListComponent?
  • Как его правильно перевести на /messages/1?
  • Должен ли я создать activeMessageId<Integer> в моем состоянии?
  • Как все эти компоненты должны быть связаны с React-Router?

Здесь мои текущие попытки:

export default (store) => {
  const loadAuth = (nextState, replaceState, next) => { ... };

  return (
    <Route name="app" component={App} path="/" onEnter={loadAuth}>
      <IndexRoute component={Content}/> // <== THIS IS A DUMMY COMPONENT. It diplays pre-loader until the app is transitioned to real first message
      <Route path="messages/:id" component={Message}/>
    </Route>
  );
};

Не могли бы вы дать мне несколько моментов, как подключить точки? Что такое логика потока данных асинхронного ввода?

Я использую isomorphic-redux пример в качестве базы для своего приложения. Хотя изоморфно, это не должно быть слишком большой разницей между обычным приложением Redux

Спасибо.

UPDATE

Одна из идей - установить onEnter hook для <IndexRoute component={Content}/>, который будет извлекать сообщения, переходить в состояние и начальный переход. Это путь redux + router?

Однако этот способ также может быть довольно сложным, потому что /messages работает только для аутентифицированных пользователей (где store.getState().auth.get('loaded') == true)

4b9b3361

Ответ 1

По моему скромному мнению, рендеринг на стороне сервера важен. Без этого вы будете обслуживать пустые страницы, которые оживают только на стороне клиента. Это серьезно повлияет на ваш SEO. Итак, если мы считаем, что рендеринг на стороне сервера важен, нам нужен способ получения данных, которые подходят для рендеринга на стороне сервера.

Глядя на документы для рендеринга на стороне сервера i.c.w. response-router, вот что мы находим:

  • Сначала мы вызываем match, передавая ему текущее местоположение и наши маршруты
  • Тогда мы называем ReactDOMServer.render, передавая ему renderProps, полученный из match

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

Это означает, что не может использовать жизненный цикл компонента. Также мы не можем использовать onEnter или любой другой крючок, который запускается только тогда, когда рендер уже запущен. На стороне сервера нам нужно получить данные перед началом рендеринга. Это означает, что нам нужно определить, что выбрать из renderProps, которое мы получаем из match.

Общим решением является статическая функция fetchData для компонента верхнего уровня. В вашем случае это может выглядеть примерно так:

export default class MailListComponent extends React.Component {
  static fetchData = (store, props) => {
    return store.dispatch(loadMessages());
  };
  // ....
}

Мы можем найти эту функцию fetchData на стороне сервера и вызвать ее там до того, как мы перейдем к рендерингу, потому что match дает нам renderProps, который содержит классы согласованных компонентов. Поэтому мы можем просто перебрать их и захватить все функции fetchData и вызвать их. Что-то вроде этого:

var fetchingComponents = renderProps.components
  // if you use react-redux, your components will be wrapped, unwrap them
  .map(component => component.WrappedComponent ? component.WrappedComponent : component)
  // now grab the fetchData functions from all (unwrapped) components that have it
  .filter(component => component.fetchData);

// Call the fetchData functions and collect the promises they return
var fetchPromises = fetchingComponents.map(component => component.fetchData(store, renderProps));

fetchData возвращает результат store.dispatch, который будет Promise. На стороне клиента это просто покажет некоторый экран loading до тех пор, пока обещание не выполнится, но на стороне сервера нам нужно будет подождать, пока это не произойдет, поэтому мы фактически имеем данные в магазине, когда мы переходим к фазе рендеринга. Мы можем использовать Promise.all для этого:

// From the components from the matched route, get the fetchData functions
Promise.all(fetchPromises)
  // Promise.all combines all the promises into one
  .then(() => {
    // now fetchData() has been run on every component in the route, and the
    // promises resolved, so we know the redux state is populated
    res.status(200);
    res.send('<!DOCTYPE html>\n' +
      ReactDOM.renderToString(
        <Html lang="en-US" store={app.store} {...renderProps} script="/assets/bridalapp-ui.js" />
      )
    );
    res.end();
})

Там вы идете. Мы отправляем клиенту полностью заполненную страницу. Там мы можем использовать onEnter или крючки жизненного цикла или любой другой удобный метод для получения последующих данных, необходимых, когда пользователь перемещается на стороне клиента. Но мы должны попытаться убедиться, что у нас есть функция или аннотация (начальное действие?), Доступная для самого компонента, поэтому мы можем предварительно получить данные для визуализации на стороне сервера.

Ответ 2

Я работаю над довольно большим приложением (React, Redux, React Router и т.д.) с очень похожими функциями (браузер сообщений с боковой панелью + панель поиска/инструменты и т.д.). Это почти идентичный структурно к тому, что вы изложили выше. Использование Реагировать на жизненный цикл компонентов для нас очень хорошо зарекомендовало себя.

В основном оставляя это для компонента, чтобы решить: "Учитывая эти данные (сообщения, загрузка и т.д.), что я должен выглядеть и/или делать?".

Мы начали с беспорядка с onEnter и другими стратегиями "вне компонента", но они стали чувствовать себя слишком сложными. Также связан ваш вопрос о хранении activeMessageId. Если я правильно понял ваш сценарий, это должно быть надежно выведено из вашего текущего маршрута params.id в этом примере.

Чтобы дать представление о некоторых вещах, которые этот подход делает для нас

введите описание изображения здесь

Конечно, этот пример немного сокращен/упрощен, но он суммирует часть "запросы сообщений" и очень близок к фактическому подходу, который работает для нас.

const MailApp = React.createClass({
  componentWillMount() {
    this._requestIfNeeded(this.props);
  },

  componentWillUpdate(newProps) {
    this._requestIfNeeded(newProps);
  },

  _requestIfNeeded(props) {
    const {
      // Currently loaded messages
      messages,

      // From our route "messages/:id" (ReactRouter)
      params: {id},

      // These are our action creators passed down from `connect`
      requestMessage,
      requestMessages,

      // Are we "loading"?
      loading,
      } = props;

    if (!id) {
      // "messages/"
      if (messages.length === 0 && !loading)
        return requestMessages();
    } else {
      // "messages/:id"
      const haveMessage = _.find(messages, {id});
      if (!haveMessage && !loading)
        return requestMessage(id);
    }
  },

  render() {
    const {
      messages,
      params: {id},
    } = props;

    return (
      <div>
        <NavBar />
        <Message message={_.find(messages, {id})}/>
        <MailList message={messages} />
      </div>
    )
  }
});

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

Ответ 3

API React-Router 2 открывает интересные возможности для получения данных, позволяя вам иметь слой между маршрутизатором и вашим приложением. Это отличное место для дополнительного плагина, такого как https://github.com/rackt/async-props или https://github.com/raisemarketplace/ground-control.

Универсальный крюк fetchData в GroundControl (специфичный для Redux) дает вам довольно много энергии. Позволяет блокировать клиентскую часть компонента-рендеринга; обрабатывать рендеринг async; а затем отправить в редуктор.

const fetchData = (done, { dispatch, clientRender }) => {
  // show blocking loading component
  setTimeout(() => {
    clientRender(); // non-blocking loader (props.loading)
    setTimeout(() => {
      dispatch(actions.load({ mydata });
      done();
    }, 1000);
  }, 1000)
};

Редукторы объявляются на маршрутах и ​​вызывается функция fetchData для каждого вложенного маршрута. Таким образом, вы можете иметь родительский макет маршрута handle auth. Затем fetchData во вложенных маршрутах обрабатывает то, что им нужно.