Когда вы пишете просто на React - то используете Redux store как глобальное хранилище - ничего сложного.

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

В React - результаты запросов сохраняем в Redux store - и уже на основании этих данных рендерится страница - всё понятно.

В Next.js же - страница отрендерилась на сервере - и пришла уже в виде html и css. Внимание вопрос: как тогда использовать Redux - если код страницы нам уже пришёл? И для чего вообще в таком случае нужен Redux при использовании Next?

Работает это примерно так: страница рендерится на сервере. Когда пользователь заходит на сайт - он скачивает эту страницу с сервера. На этом этапе серверный рендеринг закончился. Пользователь получил страницу в базовом виде - таком, как её видит весь интернет и роботы поисковиков. В этот момент в Redux store - хранятся исключительно те значения, какие там были при инициализации.

Если после этого сделать запрос к серверу и изменить значения в store - они там сохранятся. И если все ссылки для переходов по страницам сайта были обёрнуты в тег <Link></Link> - то при переходе по ним приложение будет вести себя в плане Redux - как SPA - всё, что загружено в Redux store - останется без изменений.

Например, переходим на главную страницу сайта. Получили эту страницу отрендереной с сервера. После чего залогинились - и информацию о пользователе (например, его имя) - сохранили в Redux. Тогда когда начнёте переходить на другие страницы сайта - его имя уже будет храниться в store и не придётся его каждый раз заново запрашивать.

При всём этом есть один интересный сценарий использования - как можно сочетать серверный рендеринг со store. Рассмотрим на примере:

Допустим, у нас 2 глобальных свойства приходят с бекенда:

  • язык сайта

  • идут сейчас технические работы или нет

При этом, если пользователь изменил язык сайта - мы хотим чтобы это сохранилось для всех страниц сайта (допустим, пользователь ещё не разрешил хранит кукисы).

Примерно так будет выглядеть Redux store:

Типы
export interface IGlobalSettings{
    isTechnicalWork: boolean,
    language: string,
}

globalSettingsReducer.ts
import {IGlobalSettings} from "@/types/redux_types";
import {createSlice, PayloadAction} from "@reduxjs/toolkit";

export const initialState: IGlobalSettings = {
    isTechnicalWork: false,
    language: "default",
}

export const GlobalSettingsSlice = createSlice({
    name: 'global_settings',
    initialState,
    reducers: {
        GlobalUpdateLanguage (state, action: PayloadAction<string>){
            state.language = action.payload;
        },
        GlobalUpdateTechWork (state, action: PayloadAction<boolean>){
            state.isTechnicalWork = action.payload;
        },
    }
})

export default GlobalSettingsSlice.reducer;

store.ts
import {configureStore, combineReducers} from "@reduxjs/toolkit";
import { createWrapper } from 'next-redux-wrapper';
import globalSettingsReducer from "@/redux/globalSettingsReducer";

const rootReducer = combineReducers({
    global_settings: globalSettingsReducer,
    //другие редюсеры добавлять сюда.
})
export const setupStore = () => {
    return configureStore({
        reducer: rootReducer
    })
}
export const wrapper = createWrapper(configureStore);
export type RootState = ReturnType<typeof rootReducer>
export type AppStore = ReturnType<typeof setupStore>

export type AppDispatch = AppStore['dispatch']

Определим хуки, чтобы было удобнее работать:

хуки
import {useDispatch, useSelector, TypedUseSelectorHook} from "react-redux";
import {AppDispatch, RootState} from "@/redux/store";

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

Теперь перейдём к коду тестовой страницы, на которой мы будем всё это использовать:

import {GetStaticProps} from "next";
import axios from "axios";
import {IGlobalSettings} from "@/types/redux_types";

import React, {FC, ReactNode, useEffect} from "react";
import Head from "next/head";
import {useAppDispatch, useAppSelector} from "@/hooks/redux";
import {GlobalSettingsSlice} from "@/redux/globalSettingsReducer";

export type GlobalSettingsProps = {
    p_global_settings: IGlobalSettings,
}

const TestPage:FC<GlobalSettingsProps> = ({p_global_settings}) => {
    const dispatch = useAppDispatch()
    const state_language = useAppSelector(state => state.global_settings.language)
    const state_tech_works = useAppSelector(state => state.global_settings.isTechnicalWork)

    let tech_works_string :string | ReactNode
    if (state_language == "EN") {tech_works_string = <h1>Technical works!</h1>}
    if (state_language == "RU") {tech_works_string = <h1>Технические работы</h1>}

    let main_string :string | ReactNode
    if (state_language == "EN") {main_string = <h1>Test!</h1>}
    if (state_language == "RU") {main_string = <h1>Тест!</h1>}


    useEffect(() => {
        if (state_language == "default") {
            dispatch(GlobalSettingsSlice.actions.GlobalUpdateLanguage(p_global_settings.language))
            //Обновляем язык - на полученный от сервера - если его пользователь сам не менял
        }
        dispatch(GlobalSettingsSlice.actions.GlobalUpdateTechWork(p_global_settings.isTechnicalWork))
        //Обновляем статус технических работ.
    },[]);

    return (
        <>
            <Head>
                <title>Test page</title>
                <meta name="description" content="Test page" />
                <meta name="viewport" content="width=device-width, initial-scale=1" />
                <link rel="icon" href="/favicon.ico" />
            </Head>
            <main>
                {state_tech_works ? tech_works_string : main_string}
            </main>
        </>
    )
}

export default TestPage;

export const getStaticProps: GetStaticProps = async () => {
    const response = await axios.get<IGlobalSettings>('https://api.somesite.com/global_settings')
    const test: IGlobalSettings = {
        isTechnicalWork: false,
        language: "EN",
    }
    return {
        props: {
            p_global_settings: response.data  //test   //Для тестирования без api - можно заменить response.data на test
            //В переменную isTechnicalWork получаем false,
            //В переменную language получаем "EN" - по умолчанию английский язык
        },
        revalidate: 60,
    }
}

В функции "getStaticProps" - происходит получение данных с сервера. Эта информация обновляется раз в 60 секунд - т.е. Next будет раз в минуту опрашивать сервер - что поменялось.

Полученные от сервера данные мы передаём в компоненту страницы - "TestPage".

Потом через хук useEffect мы эти данные передаём в Redux store - 1 раз после загрузки страницы.

Таким образом, у всех пользователей в store будут после загрузки страницы не те данные, которыми инициируюется store - а те данные, которые раз в минуту приходят от бекенда и рендерятся в статической странице.

Сначала мы обновляем данные в store из static props, а потом уже из store берём информацию для рендеринга той страницы, которую увидит пользователь. При изменении значений в state - будет меняться и страница.

При этом, если пользователь поменяет язык - то при переходе на другие страницы эту настройку в state не затрёт на те данные, которые от бекенда приходят в getStaticProps.

В результате, если у нас на страницу заходят 100 пользователей в минуту - то за минуту у нас будет всего одно обращение к бекенду вместо 100. Но и если никто не зайдёт - будет всё то же 1 обращение к бекенду в минуту. (тут мы говорим не про запрос личных данных пользователя - а про данные, которые одинаковые для всех пользователей. Имя пользователя придётся запрашивать всё те же 100 раз).

Таким образом, используя Next.js - можно инициировать Redux данными с бекенда, не делая каждый раз запрос к бекенду для каждого пользователя по поводу общих данных (которые одинаковые для всех пользователей) - и таким образом ощутимо уменьшить нагрузку на бекенд.

P.S: ищу удалёнку - контакты в профиле.

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


  1. markelov69
    25.05.2023 19:23
    +1

    Использовать redux это самая великая глупость. Можно было ещё скидку сделать в 2015 и 2016 году, но после это уже просто нелепо.


    1. 8bitjoey
      25.05.2023 19:23
      +1

      что вы можете предложить в качестве альтернативы?


      1. markelov69
        25.05.2023 19:23

        что вы можете предложить в качестве альтернативы?

        MobX, использую ещё с 2016 года и бед не знаю, одно удовольствие.


        1. 8bitjoey
          25.05.2023 19:23

          Сходу две проблемы:
          - с mobx вы адаптируете код под mobx. В отличии от redux'а, где компоненты продолжают использовать пропсы и им начхать откуда они пришли и куда ведут колбеки. При желании можно заменить стор.
          - из-за использования observable, стор mobx не может хранить большую коллекцию сложных объектов (это касается любых подобных решений, в т.ч., например, redux toolkit). Несколько лет назад я проводил тестирование: mobx со скрипом позволял обрабатывать 50к таких объектов, redux с легкостью 200к.

          Но это не значит, что mobx надо хоронить, как вы спешите хоронить redux. Полагаю, mobx прекрасно себя показывает в небольших приложениях.
          Redux надо уметь готовить, тогда не будет никакого особого бойлерплейта, ведь, полагаю, именно это является объектом критики.


          1. markelov69
            25.05.2023 19:23

            - с mobx вы адаптируете код под mobx. В отличии от redux'а, где компоненты продолжают использовать пропсы и им начхать откуда они пришли и куда ведут колбеки. При желании можно заменить стор.

            Всё тоже самое можно делать с MobX. Только вот props hell это дичь и от этого много лет назад ушли.

            - из-за использования observable, стор mobx не может хранить большую коллекцию сложных объектов (это касается любых подобных решений, в т.ч., например, redux toolkit). Несколько лет назад я проводил тестирование: mobx со скрипом позволял обрабатывать 50к таких объектов, redux с легкостью 200к.

            // В глубину ничего не будет реактивным, реактивная только эта ссылка
            @observable.ref items = []; // Хоть миллион объектов

            Полагаю, mobx прекрасно себя показывает в небольших приложениях.

            Не, он себя идеально показывает вообще в любых приложениях, в огромных, в средних и в мелких.

            Redux надо уметь готовить, тогда не будет никакого особого бойлерплейта, ведь, полагаю, именно это является объектом критики.

            Как ни крути, он всё равно не дотянет до MobX. Как минимум из-за:
            1) Ручной pub/sub в явном виде. (В MobX это автоматически)
            2) Обязательная иммутабильность.
            3) Неудобство работы с асинхронщиой, и костыли в виде RTK и т.п. особно погоды не делают, всё равно мягко говоря всё это так себе.

            Про вынужденный говнокод (и как бы ты не старался он 10000% будет говнокодом) из-за этого подхода я вообще молчу.


            1. 8bitjoey
              25.05.2023 19:23

              Всё тоже самое можно делать с MobX. Только вот props hell это дичь и от этого много лет назад ушли.

              С какого количества пропсов начинается hell? Почему вообще использование props считается чем-то плохим, ведь в этом вся фишка реакта - черная коробочка pure function, в которую на вход даешь одно и всегда получаешь одно и то же.

              Про вынужденный говнокод (и как бы ты не старался он 10000% будет говнокодом) из-за этого подхода я вообще молчу.

              Есть какой-нибудь пример кода с mobx, который вам не кажется говнокодом? Может быть некий open source проект.


        1. kfilipchuk
          25.05.2023 19:23

          Redux, использую ещё с 2016 года и бед не знаю, одно удовольствие.


    1. makeoverweb
      25.05.2023 19:23

      Мы в команде используем редакс и он помогает решать текущие задачи. Какой смысл засерать инструмент, если вы им не пользуйтесь(или не умеете)??


  1. kield
    25.05.2023 19:23
    +3

    Использовать стейт менеджер в Next для небольших объёмов данных не то чтобы профитно. Часто для таких задач хватает контекстного менеджера с каким-нибудь браузерным хранилищем. Так же часто используются query параметры.

    ИМХО использовать стейт менджер можно в случаях когда на странице много динамичных данных либо данные используется по всему приложению и их так же много и они динамичны

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

    Так же рекомендую ознакомится с новой документацией Next, которая включает в себя использование папки app. Код выглядит немного красивее и очевиднее. Это теперь не бета функционал. В качестве легковестной современной альтернативы Redux сущевствует Zustand.


    1. alice_berd Автор
      25.05.2023 19:23

      Проблема Next 13 и папки app в том, что его пока нельзя использовать в больших проектах - нет стабильной версии (ну 2-3 месяца назад это было так - все баги ещё не выловили).


  1. AlekseyStepp
    25.05.2023 19:23
    +1

    Пробовал работать с redux в next js проектах, показалось, что они плохо дружат.

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

    Если вы редакс используете только для каких-то общих данных, как в примерах, выбранный язык и т.п., то редакс вообще не нужен с его многослойностью и сложностью. Можно посмотреть на effector, zustand или вообще контекст


    1. alice_berd Автор
      25.05.2023 19:23

      Если на каждой странице все результаты запросов пихать в Redux - да, в Next это приведёт к росту размера страниц и их более медленной загрузке. Но для хранения данных о пользователе и каких-нибудь общих мест - почему бы и нет?