Создаем изоморфное/универсальное приложение на Next.JS + Redux

    Это вторая статья о 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
      0
      redux-actions покраше будут этих switch типов

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

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

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

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

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

                Это все звучит, но в вашем случае это скорее — я не разобрался как написать 2 строчки с create-react-app, и просто нашел жирный фреймворк ради ssr
                  0
                  Все к нему прикручивается, только не в 2 строчки, вот пример, как на моей же миддлваре это можно сделать. Сайты делают разные люди, а не только Вы или я, не все хотят знать, как оно работает изнутри.
                  0
                  А в Create React App много возникает трудностей если самому прикрутить серверный рендеринг если он из коробки не поддерживется или лучше уже сразу использовать уже готовое решение Next.js, Electrode если наперед знаешь что потребуется изоморфизм.
                    0
                    Смотря как прикручивать. Для него есть react snapshot пакет, который делает статический сайт, обходя все ссылки. Но оно не работает с Redux. Есть моя миддлвара для Router + Redux, с ней хлопот не очень много, но продукт относительно свежий, хоть и крутящийся на продакшене. Next/Electrode более матерые, но и ограничений там порядком. Попробуйте сами разные варианты, быстро станет очевидно, какой Вам больше подходит.
                  0

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

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

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

              Самое читаемое