При разработке проекта на react возник вопрос – как при переходе по внутренней ссылке сначала сделать предзагрузку данных, а потом уже отобразить компонент? Пример (задержка добавлена специально):
до

и после

Решение оказалось достаточно простое и отлично интегрировалось с серверным рендерингом.

Thunk middleware

Для начала нам понадобится redux-thunk middleware.
подключение thunk
import thunk from 'redux-thunk';
import {applyMiddleware, combineReducers, createStore } from 'redux';

export default function (initial_state = {}) {
  const root_reducer = combineReducers({
// reducers
  });

  return createStore(root_reducer, initial_state, applyMiddleware(thunk));
}
Он дает возможность action`ам возвращать не только объекты, но и функции, а результат, который вернет эта функция, будет возвращен как результат dispatch. Это позволит нам вызвать асинхронный action и дождаться результата его выполнения. Пример асинхронного вызова
redux action
export function apiRequest(api_key, api_url, options) {
  return (dispatch, getState) => {
// С помощью getState можно получить текущее состояние
let state   = getState();
let request = false;

// Указываем язык
options.USER_LANG = state.lang;

// Если запрос уже выполняется - отменяем
dispatch(abortApiRequest(api_key));

let promise = new Promise((resolve, reject) => {
  // Request - библиотека для выполнения ajax запросов
  request = Request.fetch(
    api_url, {
      success: response => {
        // Успешное выполнение
        dispatch(apiLoadSuccess(api_key, options, response));
        resolve();
      },
      error: error => {
        // Ошибка
        dispatch(apiLoadError(api_key, options, error));
        resolve();
      },

      data:    options,
      session: state.session,
    });
});

// Ставим запросу статус «загрузка»
dispatch(apiLoadStarted(api_key, request));

return promise;
  }
}
Теперь мы можем написать код вида
где-то в коде
store.dispatch(apiRequest("gallery", "/api/gallery/getPhotos", {page: 1}))
.then(() => {
  // Данные загружены в хранилище
});

Component fetchData

У тех компонентов, которым требуются данные, пропишем статичный метод fetchData. Этому методу мы будем передавать хранилище и список параметров (которые мы сформируем из GET и аргументов роута). А метод вернет список Promise-ов, выполнения которых нам нужно будет дождаться. Этот метод мы можем использовать как при серверном рендеринге, так и при предзагрузке данных на клиенте.
компонент фото-галереи
let Gallery = React.createClass({
  // typical react class
});

Gallery.fetchData = function(store, params) {
  let state = store.getState();
  let ret   = [];
  
  // Проверяем, надо ли загружать фотографии
  if (!state.api.gallery) {
ret.push(store.dispatch(apiRequest(
  "gallery", "/api/gallery/getPhotos", {
    page: params.page ? parseInt(params.page) : 1,
  }
)));
  }
  
  // Проверяем, надо ли загружать теги
  if (!state.api.gallery_tags) {
ret.push(store.dispatch(apiRequest(
  "gallery_tags", "/api/tags/getTags", {
    type: 'gallery',
  }
)));
  }
  
  return ret;
};
Теперь мы можем обратиться к методу fetchData и он выполнит загрузку данных, а мы – можем определить когда данные будут загружены и компонент можно будет отображать.

React-router onEnter

В react-router воспользуемся хуком onEnter. Если передать ему функцию с тремя аргументами, то хук будет вызван асинхронно и переход не выполнится пока мы не вызовем callback.
роут
<Route path='gallery' component={Gallery} key="gallery" onEnter={fetchComponentData} />
onEnter
function fetchComponentData(nextState, replace, callback) {...}
А использую nextState.routes мы можем обойти компонеты и собрать нужные им данные через fetchData.

Собираем все вместе

Теперь осталось только собрать все это вместе. В роуты будем передавать хранилище и добавим функции makeFetchParams и fetchComponentsData.
роуты
export default (store) => {
  // Формируем список параметров
  function makeFetchParams(location, params) {
let ret = {};

// Переменные из GET
if (location.query) {
  for (let key in location.query) {
    ret[key] = location.query[key];
  }
}

// Параметры router-а
if (params) {
  for (let key in params) {
    ret[key] = params[key];
  }
}

return ret;
  }
  
  // Хук onEnter
  function fetchComponentData(nextState, replace, callback) {
if (SCRIPT_ENV == 'server') {
  // При выполнении на сервере загрузка не требуется
  return callback();
}

// Список promise-ов
let needs = [];
// Парамерты для fetchData
let params = makeFetchParams(nextState.location, nextState.params);

// Перебираем все компоненты
nextState.routes.forEach(route => {
  let component = route.component;

  if (!component) return;
  
  // Для компонентов обернутых в connect
  if (component.wrappedComponent) {
    component = component.wrappedComponent;
  }

  // Если метода fetchData нет, то и данные загружать не надо
  if (!component.fetchData) return;

  // Добавляем к списку загрузок
  needs = needs.concat(component.fetchData(store, params));
});

if (!needs.length) {
  // Загружать ничего не надо
  return callback();
}

// Загружаем данные и делаем переход
Promise.all(needs)
.then(() => {
  callback();
})
.catch(() => {
  callback();
});
  }
  
  return (
<IndexRoute component={Landing} key="index" onEnter={fetchComponentData} />,
<Route path='gallery' component={Gallery} key="gallery" onEnter={fetchComponentData} />,
<Route path='gallery/:photo_id' component={Photo} key="gallery_photo" onEnter={fetchComponentData} />,
<Route path='*' component={Landing} onEnter="fetchComponentData" />,
  );
}
подключение роутов
ReactDOM.render(
  <Provider store={store}>
<Router history={browserHistory}>
  {routes(store)}
</Router>
  </Provider>,
  document.getElementById('react-root')
);