Как стать автором
Обновить

Next.js и Redux — для чего и как использовать вместе

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров14K

Когда вы пишете просто на 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: ищу удалёнку - контакты в профиле.

Теги:
Хабы:
+5
Комментарии14

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн