Блог

2025 / piscodev / Игорь Быстрицкий

обложка статьи
Блог

Cookie в Next

Как создавать и читать Cookie. Функции-обертки на TypeScript с удобной типизацией.

    • NextTypeScript
  • Нормально

  • 01 февраля 2024
  • Игорь Быстрицкий

Cookie в NextНа сервереНа клиентеТипизация печенийРезультат
Если понравилась статья, поделитесь с друзьями

Next / Больше статей

Что нового в Next 14

Что нового в Next 14

Next 14, выпущенный 26 октября 2023 года, ускоряет время сборки в 2 раза и добавляет несколько новых функций.

29 октября 2023
Приятные шрифт от Vercel - Geist Sans и Geist Mono

Приятные шрифт от Vercel - Geist Sans и Geist Mono

"Geist действительно олицетворяет дух программирования и дизайна в творческом сообществе Vercel" - заявляют в Vercel.

20 июня 2024
Типизация страницы в Next

Типизация страницы в Next

Используй TypeScript и Next вместе, чтобы создать кастомный тип TNextPage, с помощью которого можно с легкостью указать типы params и searchParams на любой странице.

20 июня 2024
Шрифты в Next и Tailwind

Шрифты в Next и Tailwind

Расскажу о любимом способе использовать шрифты на проектах с Next и Tailwind

30 мая 2024

Cookie в Next

На сервере

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

TypeScript

// Какой-нибудь СЕРВЕРНЫЙ модуль.

import { cookies } from 'next/headers'

const cookieStore = cookies() // cookieStore: ReadonlyRequestCookies

Теперь можно и прочитать печенье с помощью функции get:

TypeScript

// Так можно прочитать печенье, которое гласит, что юзер впервые на сайте.
const firstTimeOnSite = cookieStore.get('first-time-on-site')?.value // firstTimeOnSite: string | undefined

if (JSON.parse(firstTimeOnSite) === false) {
	console.log('Привет, давно не виделись! 😉')
}

Чтобы безошибочно преобразовать печенье в правильный тип, его нужно запарсить с помощью JSON. Иначе печенья с булевым значением true/false сохраняют как 1/0, а не true/false во избежание следующей ошибки: если сохранить печенье со строчкой false, оно все равно будет читаться строкой "false", и при проверке такого печенья оно вернет true, так как строка "false" не пуста.

Или создать печенье, используя set:

TypeScript

// Печенье будет висеть в браузере целый год (насколько я знаю, это максимальное время).
cookies().set('first-time-on-site', 1, { maxAge: 60 * 60 * 24 * 365 })

// Теперь это печенье перезапишется со значением 0, но удалится в конце сессии вообще.
cookies().set('first-time-on-site', 0)

На клиенте

Next не предполагает, что вы будете использовать печенья на клиенте. Любые функции, которые взаимодействуют с печеньями должны принадлежать коду сервера, а не клиента, в этот список входят:

  • серверные компоненты,
  • хэндлеры машрутов (Route Handlers),
  • серверные действия (Server Actions).

Свои функции по работе с печеньями можно сделать серверными действиями, объявив их в модуле с директивой "use server", например в файле src/actions.ts. Также Next требует, чтобы серверные действия были асинхронными, вне зависимости от их содержимого.

Ниже серверное действие, которое создает печенье "скрыть информационный баннер":

TypeScript

'use server'

import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'

export async function hideInfoBanner() {
	cookies().set('hide-info-banner', 1, { maxAge: 60 * 60 * 24 * 365 })
	revalidatePath('/')
}
Функция revalidatePath перезагрузит страницу по пути /.

Скорее всего, новенькая функция hideInfoBanner будет вызываться по нажатию кнопки "закрыть" или типа того. Клик будет на клиенте, но как тогда вызывать серверную функцию? Для этого необходимо использовать элемент form и его аттрибут action:

TypeScript JSX

import { getCookie, hideInfoBanner } from '../actions'

export default async function Home() {
	const ifHideInfoBanner = await getCookie('hide-info-banner')

	return (
		<main>
			{!ifHideInfoBanner && (
				<article>
					<div>
						<h1>Впервые на сайте?</h1>
						<h2>
							Узнайте больше о проекте на <Link href={'/info'}>этой странице</Link>.
						</h2>
					</div>
					<form action={hideInfoBanner}>
						{/** Нажимая на кнопку, форма вызовет серверную функцию. */}
						<button type='submit'>
							<TbX />
						</button>
					</form>
				</article>
			)}
		</main>
	)
}

Типизация печений

Когда вы знаете, какие печенья будут в приложении, воспользуйтесь помощью TypeScript.

Допустим, вы хотите создать печенье для хранения текущего языка, таки у вас дорогой мультиязычный сайт, поддерживающий 3 языка, включая русский, английский и даже китайский:

TypeScript

cookies().set('lang', 'ru')
cookies().set('lang', 'en')
cookies().set('lang', 'ch')

Все здорово, но никто не мешает вам установить печенья языка на французский язык, вдруг вы случайно случайно подумали, что он поддерживается:

TypeScript

cookies().set('lang', 'fr')

Во избежание такой ошибки нам нужно создать вспомогательную функцию с нужными типами:

TypeScript

function setLangCookie(lang: 'ru' | 'en' | 'ch') {
	cookies().set('lang', lang, { maxAge: 60 * 60 * 24 * 365 })
}

setLangCookie('ru') // Можно.
setLangCookie('fr') // Не можно.

Но вот у нас на сайте теперь появилась темная тема, создавать для нее идентичную функцию будет странно. Лучше создать функцию общего назначения, которая бы позволила нам взаимодействовать с любым печеньем нашего приложения, а типы печений тогда прописать в другом месте:

TypeScript

import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import { cookies } from 'next/headers'

type TCookies = {
	lang: 'ru' | 'en' | 'ch' | undefined // Печенья может и не быть.
	dark: true | undefined // Темная тема либо включена, либо печенья не будет вообще и включится белая.
}

export function setCookie<N extends keyof TCookies>(name: N, value: NonNullable<TCookies[N]>, options?: Partial<ResponseCookie>) {
	store.set(name, JSON.stringify(value), options)
}

setCookie позволяет нам выбрать лишь lang или dark а тип 2-го параметра соответствует тому, что мы указали в TCookies:

TypeScript

setCookie('lang', 'ru')
setCookie('lang', 'fr') // Нельзя.
setCookie('theme', true)
setCookie('theme', undefined) // Нельзя. Лучше печенье эксплицитно удалить.

Результат

Полный набор функций для работы с печеньями находится в серверном модуле actions.ts (название может быть любым) и выглядит так:

TypeScript

'use server'

import { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
import { cookies } from 'next/headers'

// Объявление печений.
type TCookies = {
	lang: 'ru' | 'en' | 'ch' | undefined
	dark: true | undefined
	'hide-info-banner': true | undefined
}

// Создание или перезапись печенья.
export async function setCookie<N extends keyof TCookies>(name: N, value: NonNullable<TCookies[N]>, options?: Partial<ResponseCookie>) {
	cookies().set(name, JSON.stringify(value), options)
}

// Чтение печенья.
export async function getCookie<N extends keyof TCookies>(name: N) {
	const value = cookies().get(name)?.value
	if (!value) return
	return JSON.parse(value) as TCookies[N]
}

// Проверка наличия печенья.
export async function hasCookie<N extends keyof TCookies>(name: N) {
	return !!cookies().get(name)?.value
}

// Удаление печенья.
export async function deleteCookie<N extends keyof TCookies>(name: N) {
	return !!cookies().delete(name)
}

// Здесь же желательно сразу объявить функции, которые будут использоваться в формах клиентских компонентов, например функция hideInfoBanner.

export async function hideInfoBanner() {
	setCookie('hide-info-banner', true, { maxAge: 60 * 60 * 24 * 365 })
	revalidatePath('/')
}