Когда вы пишете просто на 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)
kield
25.05.2023 19:23+3Использовать стейт менеджер в Next для небольших объёмов данных не то чтобы профитно. Часто для таких задач хватает контекстного менеджера с каким-нибудь браузерным хранилищем. Так же часто используются query параметры.
ИМХО использовать стейт менджер можно в случаях когда на странице много динамичных данных либо данные используется по всему приложению и их так же много и они динамичны
Мне пока не довелось встретить кейс, в котором для решения задачи нужно было бы тащить стейт менеджер. С грамотной декомпозицией и архитектурой можно обходиться и без него.
Так же рекомендую ознакомится с новой документацией Next, которая включает в себя использование папки app. Код выглядит немного красивее и очевиднее. Это теперь не бета функционал. В качестве легковестной современной альтернативы Redux сущевствует Zustand.
alice_berd Автор
25.05.2023 19:23Проблема Next 13 и папки app в том, что его пока нельзя использовать в больших проектах - нет стабильной версии (ну 2-3 месяца назад это было так - все баги ещё не выловили).
AlekseyStepp
25.05.2023 19:23+1Пробовал работать с redux в next js проектах, показалось, что они плохо дружат.
Если вы хотите результаты всех запросов за данными на разных страницах складывать в релакс стор, то это плохой путь. Next сам из коробки делает код сплиттинг, но весь редаксовый код, со всеми сагами и редьюсерами будет одним чанком. По мере роста приложения будет расти количество загруженного и неиспользуемого кода на каждой странице. Для кэширования запросов лучше использовать реакт query, SWR и т.п. и не тащить все эти данные в стор.
Если вы редакс используете только для каких-то общих данных, как в примерах, выбранный язык и т.п., то редакс вообще не нужен с его многослойностью и сложностью. Можно посмотреть на effector, zustand или вообще контекст
alice_berd Автор
25.05.2023 19:23Если на каждой странице все результаты запросов пихать в Redux - да, в Next это приведёт к росту размера страниц и их более медленной загрузке. Но для хранения данных о пользователе и каких-нибудь общих мест - почему бы и нет?
markelov69
Использовать redux это самая великая глупость. Можно было ещё скидку сделать в 2015 и 2016 году, но после это уже просто нелепо.
8bitjoey
что вы можете предложить в качестве альтернативы?
markelov69
MobX, использую ещё с 2016 года и бед не знаю, одно удовольствие.
8bitjoey
Сходу две проблемы:
- с mobx вы адаптируете код под mobx. В отличии от redux'а, где компоненты продолжают использовать пропсы и им начхать откуда они пришли и куда ведут колбеки. При желании можно заменить стор.
- из-за использования observable, стор mobx не может хранить большую коллекцию сложных объектов (это касается любых подобных решений, в т.ч., например, redux toolkit). Несколько лет назад я проводил тестирование: mobx со скрипом позволял обрабатывать 50к таких объектов, redux с легкостью 200к.
Но это не значит, что mobx надо хоронить, как вы спешите хоронить redux. Полагаю, mobx прекрасно себя показывает в небольших приложениях.
Redux надо уметь готовить, тогда не будет никакого особого бойлерплейта, ведь, полагаю, именно это является объектом критики.
markelov69
Всё тоже самое можно делать с MobX. Только вот props hell это дичь и от этого много лет назад ушли.
Не, он себя идеально показывает вообще в любых приложениях, в огромных, в средних и в мелких.
Как ни крути, он всё равно не дотянет до MobX. Как минимум из-за:
1) Ручной pub/sub в явном виде. (В MobX это автоматически)
2) Обязательная иммутабильность.
3) Неудобство работы с асинхронщиой, и костыли в виде RTK и т.п. особно погоды не делают, всё равно мягко говоря всё это так себе.
Про вынужденный говнокод (и как бы ты не старался он 10000% будет говнокодом) из-за этого подхода я вообще молчу.
8bitjoey
С какого количества пропсов начинается hell? Почему вообще использование props считается чем-то плохим, ведь в этом вся фишка реакта - черная коробочка pure function, в которую на вход даешь одно и всегда получаешь одно и то же.
Есть какой-нибудь пример кода с mobx, который вам не кажется говнокодом? Может быть некий open source проект.
kfilipchuk
Redux, использую ещё с 2016 года и бед не знаю, одно удовольствие.
makeoverweb
Мы в команде используем редакс и он помогает решать текущие задачи. Какой смысл засерать инструмент, если вы им не пользуйтесь(или не умеете)??