Skip to content

Commit dd582a7

Browse files
committed
refactor: standardize using cookie as persist state (#187)
* refactor: standardize cookie handling and js-cookie dependency - Create cookie utility functions (getCookie, setCookie, removeCookie) - Replace js-cookie usage in layout-context, authStore, and authenticated-layout - Remove js-cookie and @types/js-cookie dependencies - Update prettier import order configuration - Maintain consistent cookie pattern across codebase * refactor: update context providers to use cookie-based storage - Replace localStorage with cookie handling for direction, font, and theme contexts. - Introduce utility functions for cookie management (getCookie, setCookie, removeCookie). - Ensure consistent cookie usage across all context providers for improved state persistence. * refactor: simplify layout and search context providers
1 parent 7e7c68b commit dd582a7

11 files changed

Lines changed: 104 additions & 56 deletions

File tree

.prettierrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"^zod$",
2424
"^axios$",
2525
"^date-fns$",
26-
"^js-cookie$",
2726
"^react-hook-form$",
2827
"^use-intl$",
2928
"^@radix-ui/(.*)$",

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
"cmdk": "1.1.1",
4444
"date-fns": "^4.1.0",
4545
"input-otp": "^1.4.2",
46-
"js-cookie": "^3.0.5",
4746
"lucide-react": "^0.525.0",
4847
"react": "^19.1.0",
4948
"react-day-picker": "9.8.0",
@@ -66,7 +65,6 @@
6665
"@tanstack/react-router-devtools": "^1.129.0",
6766
"@tanstack/router-plugin": "^1.129.0",
6867
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
69-
"@types/js-cookie": "^3.0.6",
7068
"@types/node": "^24.0.15",
7169
"@types/react": "^19.1.8",
7270
"@types/react-dom": "^19.1.6",

pnpm-lock.yaml

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/layout/authenticated-layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import Cookies from 'js-cookie'
21
import { Outlet } from '@tanstack/react-router'
2+
import { getCookie } from '@/lib/cookies'
33
import { cn } from '@/lib/utils'
44
import { LayoutProvider } from '@/context/layout-context'
55
import { SearchProvider } from '@/context/search-context'
@@ -23,7 +23,7 @@ interface Props {
2323
}
2424

2525
export function AuthenticatedLayout({ children }: Props) {
26-
const defaultOpen = Cookies.get('sidebar_state') !== 'false'
26+
const defaultOpen = getCookie('sidebar_state') !== 'false'
2727
return (
2828
<SearchProvider>
2929
<SidebarProvider defaultOpen={defaultOpen}>

src/context/direction-context.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { createContext, useContext, useEffect, useState } from 'react'
22
import { DirectionProvider as RdxDirProvider } from '@radix-ui/react-direction'
3+
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
34

45
export type Direction = 'ltr' | 'rtl'
56

67
const DEFAULT_DIRECTION = 'ltr'
8+
const DIRECTION_COOKIE_NAME = 'dir'
9+
const DIRECTION_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
710

811
interface DirectionContextType {
912
defaultDir: Direction
@@ -21,7 +24,7 @@ const DirectionContext = createContext<DirectionContextType>({
2124

2225
export function DirectionProvider({ children }: { children: React.ReactNode }) {
2326
const [dir, _setDir] = useState<Direction>(
24-
() => (localStorage.getItem('dir') as Direction) || DEFAULT_DIRECTION
27+
() => (getCookie(DIRECTION_COOKIE_NAME) as Direction) || DEFAULT_DIRECTION
2528
)
2629

2730
useEffect(() => {
@@ -31,17 +34,22 @@ export function DirectionProvider({ children }: { children: React.ReactNode }) {
3134

3235
const setDir = (dir: Direction) => {
3336
_setDir(dir)
34-
localStorage.setItem('dir', dir)
37+
setCookie(DIRECTION_COOKIE_NAME, dir, DIRECTION_COOKIE_MAX_AGE)
3538
}
3639

3740
const resetDir = () => {
3841
_setDir(DEFAULT_DIRECTION)
39-
localStorage.removeItem('dir')
42+
removeCookie(DIRECTION_COOKIE_NAME)
4043
}
4144

4245
return (
4346
<DirectionContext
44-
value={{ defaultDir: DEFAULT_DIRECTION, dir, setDir, resetDir }}
47+
value={{
48+
defaultDir: DEFAULT_DIRECTION,
49+
dir,
50+
setDir,
51+
resetDir,
52+
}}
4553
>
4654
<RdxDirProvider dir={dir}>{children}</RdxDirProvider>
4755
</DirectionContext>
@@ -52,7 +60,7 @@ export function DirectionProvider({ children }: { children: React.ReactNode }) {
5260
export function useDirection() {
5361
const context = useContext(DirectionContext)
5462
if (!context) {
55-
throw new Error('useDirection must be used within a DirectionContext')
63+
throw new Error('useDirection must be used within a DirectionProvider')
5664
}
5765
return context
5866
}

src/context/font-context.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import React, { createContext, useContext, useEffect, useState } from 'react'
22
import { fonts } from '@/config/fonts'
3+
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
34

45
type Font = (typeof fonts)[number]
56

7+
const FONT_COOKIE_NAME = 'font'
8+
const FONT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
9+
610
interface FontContextType {
711
font: Font
812
setFont: (font: Font) => void
13+
resetFont: () => void
914
}
1015

1116
const FontContext = createContext<FontContextType | undefined>(undefined)
@@ -14,7 +19,7 @@ export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
1419
children,
1520
}) => {
1621
const [font, _setFont] = useState<Font>(() => {
17-
const savedFont = localStorage.getItem('font')
22+
const savedFont = getCookie(FONT_COOKIE_NAME)
1823
return fonts.includes(savedFont as Font) ? (savedFont as Font) : fonts[0]
1924
})
2025

@@ -31,11 +36,18 @@ export const FontProvider: React.FC<{ children: React.ReactNode }> = ({
3136
}, [font])
3237

3338
const setFont = (font: Font) => {
34-
localStorage.setItem('font', font)
39+
setCookie(FONT_COOKIE_NAME, font, FONT_COOKIE_MAX_AGE)
3540
_setFont(font)
3641
}
3742

38-
return <FontContext value={{ font, setFont }}>{children}</FontContext>
43+
const resetFont = () => {
44+
removeCookie(FONT_COOKIE_NAME)
45+
_setFont(fonts[0])
46+
}
47+
48+
return (
49+
<FontContext value={{ font, setFont, resetFont }}>{children}</FontContext>
50+
)
3951
}
4052

4153
// eslint-disable-next-line react-refresh/only-export-components

src/context/layout-context.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createContext, useContext, useEffect, useState } from 'react'
2-
import Cookies from 'js-cookie'
2+
import { getCookie, setCookie } from '@/lib/cookies'
33

44
export type Collapsible = 'offcanvas' | 'icon' | 'none'
55
export type Variant = 'inset' | 'sidebar' | 'floating'
@@ -40,28 +40,26 @@ interface LayoutProviderProps {
4040
children: React.ReactNode
4141
}
4242

43-
// Helper function to set cookie
44-
function setCookie(name: string, value: string): void {
45-
document.cookie = `${name}=${value}; path=/; max-age=${LAYOUT_COOKIE_MAX_AGE}`
46-
}
47-
4843
export function LayoutProvider({ children }: LayoutProviderProps) {
4944
const [collapsible, setCollapsible] = useState<Collapsible>(
5045
() =>
51-
(Cookies.get(LAYOUT_COLLAPSIBLE_COOKIE_NAME) as Collapsible) ||
46+
(getCookie(LAYOUT_COLLAPSIBLE_COOKIE_NAME) as Collapsible) ||
5247
DEFAULT_COLLAPSIBLE
5348
)
5449
const [variant, setVariant] = useState<Variant>(
55-
() =>
56-
(Cookies.get(LAYOUT_VARIANT_COOKIE_NAME) as Variant) || DEFAULT_VARIANT
50+
() => (getCookie(LAYOUT_VARIANT_COOKIE_NAME) as Variant) || DEFAULT_VARIANT
5751
)
5852

5953
useEffect(() => {
60-
setCookie(LAYOUT_COLLAPSIBLE_COOKIE_NAME, collapsible)
54+
setCookie(
55+
LAYOUT_COLLAPSIBLE_COOKIE_NAME,
56+
collapsible,
57+
LAYOUT_COOKIE_MAX_AGE
58+
)
6159
}, [collapsible])
6260

6361
useEffect(() => {
64-
setCookie(LAYOUT_VARIANT_COOKIE_NAME, variant)
62+
setCookie(LAYOUT_VARIANT_COOKIE_NAME, variant, LAYOUT_COOKIE_MAX_AGE)
6563
}, [variant])
6664

6765
const resetLayout = () => {
@@ -70,7 +68,7 @@ export function LayoutProvider({ children }: LayoutProviderProps) {
7068
}
7169

7270
return (
73-
<LayoutContext.Provider
71+
<LayoutContext
7472
value={{
7573
defaultCollapsible: DEFAULT_COLLAPSIBLE,
7674
defaultVariant: DEFAULT_VARIANT,
@@ -82,7 +80,7 @@ export function LayoutProvider({ children }: LayoutProviderProps) {
8280
}}
8381
>
8482
{children}
85-
</LayoutContext.Provider>
83+
</LayoutContext>
8684
)
8785
}
8886

src/context/search-context.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ export function SearchProvider({ children }: Props) {
2727
}, [])
2828

2929
return (
30-
<SearchContext.Provider value={{ open, setOpen }}>
30+
<SearchContext value={{ open, setOpen }}>
3131
{children}
3232
<CommandMenu />
33-
</SearchContext.Provider>
33+
</SearchContext>
3434
)
3535
}
3636

@@ -39,7 +39,7 @@ export const useSearch = () => {
3939
const searchContext = React.useContext(SearchContext)
4040

4141
if (!searchContext) {
42-
throw new Error('useSearch has to be used within <SearchContext.Provider>')
42+
throw new Error('useSearch has to be used within SearchProvider')
4343
}
4444

4545
return searchContext

src/context/theme-context.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { createContext, useContext, useEffect, useState, useMemo } from 'react'
2+
import { getCookie, setCookie, removeCookie } from '@/lib/cookies'
23

34
type Theme = 'dark' | 'light' | 'system'
45
type ResolvedTheme = Exclude<Theme, 'system'>
56

67
const DEFAULT_THEME = 'system'
8+
const THEME_COOKIE_NAME = 'vite-ui-theme'
9+
const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
710

811
type ThemeProviderProps = {
912
children: React.ReactNode
@@ -27,16 +30,16 @@ const initialState: ThemeProviderState = {
2730
resetTheme: () => {},
2831
}
2932

30-
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
33+
const ThemeContext = createContext<ThemeProviderState>(initialState)
3134

3235
export function ThemeProvider({
3336
children,
3437
defaultTheme = DEFAULT_THEME,
35-
storageKey = 'vite-ui-theme',
38+
storageKey = THEME_COOKIE_NAME,
3639
...props
3740
}: ThemeProviderProps) {
3841
const [theme, _setTheme] = useState<Theme>(
39-
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
42+
() => (getCookie(storageKey) as Theme) || defaultTheme
4043
)
4144

4245
// Optimized: Memoize the resolved theme calculation to prevent unnecessary re-computations
@@ -73,16 +76,16 @@ export function ThemeProvider({
7376
}, [theme, resolvedTheme])
7477

7578
const setTheme = (theme: Theme) => {
76-
localStorage.setItem(storageKey, theme)
79+
setCookie(storageKey, theme, THEME_COOKIE_MAX_AGE)
7780
_setTheme(theme)
7881
}
7982

8083
const resetTheme = () => {
81-
localStorage.removeItem(storageKey)
84+
removeCookie(storageKey)
8285
_setTheme(DEFAULT_THEME)
8386
}
8487

85-
const value = {
88+
const contextValue = {
8689
defaultTheme,
8790
resolvedTheme,
8891
resetTheme,
@@ -91,15 +94,15 @@ export function ThemeProvider({
9194
}
9295

9396
return (
94-
<ThemeProviderContext.Provider {...props} value={value}>
97+
<ThemeContext value={contextValue} {...props}>
9598
{children}
96-
</ThemeProviderContext.Provider>
99+
</ThemeContext>
97100
)
98101
}
99102

100103
// eslint-disable-next-line react-refresh/only-export-components
101104
export const useTheme = () => {
102-
const context = useContext(ThemeProviderContext)
105+
const context = useContext(ThemeContext)
103106

104107
if (context === undefined)
105108
throw new Error('useTheme must be used within a ThemeProvider')

src/lib/cookies.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* Cookie utility functions using manual document.cookie approach
3+
* Replaces js-cookie dependency for better consistency
4+
*/
5+
6+
const DEFAULT_MAX_AGE = 60 * 60 * 24 * 7 // 7 days
7+
8+
/**
9+
* Get a cookie value by name
10+
*/
11+
export function getCookie(name: string): string | undefined {
12+
if (typeof document === 'undefined') return undefined
13+
14+
const value = `; ${document.cookie}`
15+
const parts = value.split(`; ${name}=`)
16+
if (parts.length === 2) {
17+
const cookieValue = parts.pop()?.split(';').shift()
18+
return cookieValue
19+
}
20+
return undefined
21+
}
22+
23+
/**
24+
* Set a cookie with name, value, and optional max age
25+
*/
26+
export function setCookie(
27+
name: string,
28+
value: string,
29+
maxAge: number = DEFAULT_MAX_AGE
30+
): void {
31+
if (typeof document === 'undefined') return
32+
33+
document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`
34+
}
35+
36+
/**
37+
* Remove a cookie by setting its max age to 0
38+
*/
39+
export function removeCookie(name: string): void {
40+
if (typeof document === 'undefined') return
41+
42+
document.cookie = `${name}=; path=/; max-age=0`
43+
}

0 commit comments

Comments
 (0)