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

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


ссылка на оригинал статьи https://habr.com/ru/articles/737666/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *