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

и после

Решение оказалось достаточно простое и отлично интегрировалось с серверным рендерингом.
до
и после
Решение оказалось достаточно простое и отлично интегрировалось с серверным рендерингом.
Thunk middleware
Для начала нам понадобится redux-thunk middleware.Он дает возможность action`ам возвращать не только объекты, но и функции, а результат, который вернет эта функция, будет возвращен как результат dispatch.
Это позволит нам вызвать асинхронный action и дождаться результата его выполнения.
Пример асинхронного вызоваТеперь мы можем написать код вида
подключение 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)); }
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-ов, выполнения которых нам нужно будет дождаться.
Этот метод мы можем использовать как при серверном рендеринге, так и при предзагрузке данных на клиенте.Теперь мы можем обратиться к методу fetchData и он выполнит загрузку данных, а мы – можем определить когда данные будут загружены и компонент можно будет отображать.
компонент фото-галереи
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; };
React-router onEnter
В react-router воспользуемся хуком onEnter. Если передать ему функцию с тремя аргументами, то хук будет вызван асинхронно и переход не выполнится пока мы не вызовем callback.А использую nextState.routes мы можем обойти компонеты и собрать нужные им данные через fetchData.
роут
<Route path='gallery' component={Gallery} key="gallery" onEnter={fetchComponentData} />
onEnter
function fetchComponentData(nextState, replace, callback) {...}
Собираем все вместе
Теперь осталось только собрать все это вместе.
В роуты будем передавать хранилище и добавим функции 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') );