Это вторая статья о Server Side Rendering и изоморфных/универсальных приложениях на React. Первая под названием "Упрощаем универсальное/изоморфное приложение на React + Router + Redux + Express" была больше про кастомное решение, эта же статья нацелена больше на тех, кому не хочется заморачиваться, а хочется готовое решение, с коммьюнити, и вообще поменьше головной боли с настройкой, отладкой, подбором библиотек и т.д.


+


В данной статье будем рассматривать Next.JS, который обладает преимуществами в виде отсутствия конфигурации, серверного рендеринга и готовой экосистемы.


Из коробки Next.JS не умеет работать с Redux, поэтому в процессе написания пробного проекта я выделил получившийся общий код в отдельный репозиторий next-redux-wrapper, с помощью которого в этой статье мы и соберем приложение-пример на Next.JS + Redux.


О чем все это


Для новопришедших напоминаю, что суть серверного рендеринга довольно проста: на сервере нам нужно определить на основе правил роутера, какой компонент будет показан на странице, выяснить, какие данные ему нужны для работы, запросить эти данные, отрендерить HTML, и выслать этот HTML вместе с данными на клиент.


Сейчас стремительно набирают популярность решения типа Create React App, которые постулируют подход "ноль конфигурации", все ставится одной командой, работает прямо из коробки, но содержит кучу ограничений, к тому же серверным рендерингом не обладает совсем. Можете ознакомиться с моей статьей на эту тему: Что взять за основу React приложения.


Для этого есть альтернативы типа Next.JS и Electrode. Почему бы не взять их и забыть о мучениях? Или не забыть? Может все только хуже станет. Но на самом деле все зависит от задачи, и чтобы быстро сляпать приложение обычно гибкости хватает, но вот некоторые ограничения, о которых стоит помнить, начиная работу с Next.JS:


  1. В нем нет React Router, роутинг из коробки очень тупой и даже путь с подстановками вида /path/:id/:foo не умеет (для этого есть отдельные решения), но хотя бы есть поддержка query.
  2. Не поддерживает импорт CSS/LESS/SASS и т.д., вместо этого там CSS in JSX, но можно добавить стили напрямую в документ, но тогда не будет работать их Hot Reload.
  3. Конфигурация Webpack (который используется внутри) хоть и может быть изменена вручную, но добавление Loader'ов сильно не рекомендуется.

Жизненный цикл Next.js


В процессе рендеринга страниц мини-роутер берет соответствующий файл из директории ./pages, берет из него default export и использует статичный метод getInitialProps из экспортированного компонента для того, чтоб забросить в него эти самые props. Метод может быть асинхронным, т.е. возвращать Promise. Этот метод вызывается как на сервере, так и на клиенте, разница только в количестве получаемых аргументов, например, на сервере есть req (который является NodeJS Request). И клиент и сервер получают нормализованные pathname и query.


Примерно так выглядит код для страницы:


export default class Page extends Component {
  getInitialProps({pathname, query}) {        
    return {custom: 'custom'}; // это станет начальным набором props у страницы
  }
  render() {
    return (
      <div>
        <div>Prop from getInitialProps {this.props.custom}</div>
      </div>
    )
  }
}

Или в функциональном стиле:


const Page = ({custom}) => (
  <div>
    <div>Prop from getInitialProps {this.props.custom}</div>
  </div>
);
Page.getInitialProps = ({pathname, query}) => ({        
  custom: 'custom'
});
export default Page;

Статичный метод getInitialProps идеально подходит в качестве места, где мы можем отдиспатчить неоторые экшны, чтобы привести Redux Store в нужное состояние, чтоб затем во время рендеринга страница могла считать оттуда все нужные данные.


getInitialProps({store, pathname, query}) {
  // component will read it from store's state when rendered
  store.dispatch({type: 'FOO', payload: 'foo'});
  // pass some custom props to component
  return {custom: 'custom'}; 
}

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


Помимо аргумента getInitialProps этот же Store должен быть предоставлен Redux Provider'у, чтобы все вложенные в него компоненты могли получить доступ к Store.


Для этой цели лучше всего подходит концепция Higher Order Components. В двух слова это функция которая принимает компонент в качестве аргумента и возвращает его обернутую версию, которая тоже является компонентом, например React Router withRouter(Cmp). Иногда функция может принимать аргументы, и возвращать другую функцию, которая уже примет компонент, это называется каррирование, например React Redux connect(mapStateToProps, mapDispathToProps)(Cmp). Обертка, которую мы собираемся использовать, должна быть применена ко всем страницам, чтобы быть уверенным, что все начальные условия всегда одинаковы.


Создание приложения


Для начала, поставим все пакеты:


npm install next-redux-wrapper next@^2.0.0-beta redux react-redux --save

Создадим вещи, необходимые для Redux:


import React, {Component} from "react";
import {createStore} from "redux";

const reducer = (state = {foo: ''}, action) => {
    switch (action.type) {
        case 'FOO':
            return {...state, foo: action.payload};
        default:
            return state
    }
};

const makeStore = (initialState) => {
    return createStore(reducer, initialState);
};

Теперь обернем страницу в next-redux-wrapper:


import withRedux from "next-redux-wrapper";

const Page = ({foo, custom}) => (
  <div>
    <div>Prop from Redux {foo}</div>
    <div>Prop from getInitialProps {custom}</div>
  </div>
);

Page.getInitialProps = ({store, isServer, pathname, query}) => {
  store.dispatch({type: 'FOO', payload: 'foo'});
  return {custom: 'custom'}; 
};

// здесь мы берем makeStore и отдаем его обертке
Page = withRedux(makeStore, (state) => ({foo: state.foo}))(Page);

export default Page;

Диспатчить можно и Promise, в этом случае нужно будет дождаться его завершения и уже тогда вернуть initial props.


Вот, собственно, и все. Вся магия происходит внутри обертки, а снаружи мы видим чистую и красивую имплементацию. Полный пример можно посмотреть в репозитории Next.js: https://github.com/zeit/next.js/blob/master/examples/with-redux/README.md или посмотреть на пример в репозитории обертки: https://github.com/kirill-konshin/next-redux-wrapper/blob/master/pages/index.js.


Для запуска достаточно просто написать в консоли:


node_modules/.bin/next

P.S.


Я обнаружил один неприятный момент, если вы хотите использовать pages/_document.js (который позволяет собрать шаблон всей страницы) и диспатчить экшны из его getInitialProps, а также с конечной страницы, то может возникнуть race condition. Порядок вызова этих функций не особо контролируется и может так получиться, что они будут работать параллельно, поэтому в моменты когда рендерится шаблон страницы и сама страница состояние может быть разным.


Сама обертка этот сценарий поддерживает, потому что она сохраняет Store в самом запросе, а также гарантирует, что на клиенте Store всегда один. Однако авторы Next.js говорят, что это плохая практика иметь диспатчи в _document.js. Они предлагают вместо этого написать еше один HOC, уже кастомный, и делать все там, но это остается за рамками статьи.

Поделиться с друзьями
-->

Комментарии (12)


  1. auine
    10.03.2017 10:45

    redux-actions покраше будут этих switch типов

    Сейчас стремительно набирают популярность решения типа Create React App, которые постулируют подход «ноль конфигурации», все ставится одной командой, работает прямо из коробки, но содержит кучу ограничений, к тому же серверным рендерингом не обладает совсем.

    Никто не мешает импортнуть конфиг приложения и добавить буквально пару строчек для всего, что надо. Просто из коробки, там действительно 0 конфигурации и хватает вполне себе большенству проектов


    1. auine
      10.03.2017 11:06

      Более того серверсайд рендеринг попросту никак не ограничивается Create React App.


      1. dfuse
        10.03.2017 11:22
        +1

        Он есть из коробки? Нет. Рассматривается исключительно стандартная поставка. Любые навороты сверху я лучше буду делать с полным контролем, зачем мне Create React App для этого? Или возьму другой фреймворк.


        1. comerc
          10.03.2017 11:54
          +1

          Меня коробит ощущение, что в Next.js получается очередной монолит. Проходили в Meteor-е.


          1. dfuse
            10.03.2017 11:59

            Попробуйте Electode от Walmart Labs, там подход другой. Next тоже по-своему прекрасен.


          1. auine
            11.03.2017 13:49

            Да кстате, уж слишком много своего навязывается.


        1. auine
          11.03.2017 13:55

          Бредовый подход. Я лучше возьму более кастомизированный инструмент чем монолитное гуано которое едвали рассширяется.
          Что там делать того ssr? Подготовить стор под redux на серваке? Боже ну это просто непосильная задача

          буду делать с полным контролем

          Или возьму другой фреймворк.

          Это все звучит, но в вашем случае это скорее — я не разобрался как написать 2 строчки с create-react-app, и просто нашел жирный фреймворк ради ssr


          1. dfuse
            11.03.2017 19:44

            Все к нему прикручивается, только не в 2 строчки, вот пример, как на моей же миддлваре это можно сделать. Сайты делают разные люди, а не только Вы или я, не все хотят знать, как оно работает изнутри.


        1. inickua
          12.03.2017 07:09

          А в Create React App много возникает трудностей если самому прикрутить серверный рендеринг если он из коробки не поддерживется или лучше уже сразу использовать уже готовое решение Next.js, Electrode если наперед знаешь что потребуется изоморфизм.


          1. dfuse
            12.03.2017 07:11

            Смотря как прикручивать. Для него есть react snapshot пакет, который делает статический сайт, обходя все ссылки. Но оно не работает с Redux. Есть моя миддлвара для Router + Redux, с ней хлопот не очень много, но продукт относительно свежий, хоть и крутящийся на продакшене. Next/Electrode более матерые, но и ограничений там порядком. Попробуйте сами разные варианты, быстро станет очевидно, какой Вам больше подходит.


      1. comerc
        10.03.2017 11:51

        Может у вас есть на примете готовое решение для SSR?


        1. dfuse
          10.03.2017 11:57

          Ну например Electrode, вполне конкурент. Я его рассматриваю в статье со сравнением. Но и сам Next очень хорош, если ограничения не смущают. Только к CRA это не относится.