Skip to content

Commit c89d019

Browse files
authored
fix: #346 ensure setTheme Receives the Latest State (#347)
* fix: change setTheme function to correct handle passed callback - Updated setTheme to properly handle passed callback and always pass relevant state value to callback - Added saveToLS function to save values to localstorage * test: add unit test to check the provided setTheme fix
1 parent 3a43743 commit c89d019

File tree

2 files changed

+50
-15
lines changed

2 files changed

+50
-15
lines changed

next-themes/__tests__/index.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,32 @@ describe('setTheme', () => {
459459
expect(result.current.theme).toBe('light')
460460
expect(result.current.resolvedTheme).toBe('light')
461461
})
462+
463+
test('setTheme(<function>) gets relevant state value', () => {
464+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { })
465+
466+
const { result } = renderHook(() => useTheme(), {
467+
wrapper: ({ children }) => <ThemeProvider defaultTheme="light">{children}</ThemeProvider>
468+
})
469+
470+
act(() => {
471+
result.current.setTheme((theme) => {
472+
console.log('1', theme)
473+
return theme === 'dark' ? 'light' : 'dark'
474+
})
475+
result.current.setTheme((theme) => {
476+
console.log('2', theme)
477+
return theme === 'light' ? 'dark' : 'light'
478+
})
479+
})
480+
481+
expect(consoleSpy).toHaveBeenCalledWith('1', 'light')
482+
expect(consoleSpy).toHaveBeenCalledWith('2', 'dark')
483+
expect(result.current.theme).toBe('light')
484+
485+
consoleSpy.mockRestore()
486+
})
487+
462488
})
463489

464490
describe('inline script', () => {

next-themes/src/index.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,16 @@ const colorSchemes = ['light', 'dark']
88
const MEDIA = '(prefers-color-scheme: dark)'
99
const isServer = typeof window === 'undefined'
1010
const ThemeContext = React.createContext<UseThemeProps | undefined>(undefined)
11-
const defaultContext: UseThemeProps = { setTheme: _ => {}, themes: [] }
11+
const defaultContext: UseThemeProps = { setTheme: _ => { }, themes: [] }
12+
13+
const saveToLS = (storageKey: string, value: string) => {
14+
// Save to storage
15+
try {
16+
localStorage.setItem(storageKey, value)
17+
} catch (e) {
18+
// Unsupported
19+
}
20+
}
1221

1322
export const useTheme = () => React.useContext(ThemeContext) ?? defaultContext
1423

@@ -79,20 +88,20 @@ const Theme = ({
7988
enable?.()
8089
}, [nonce])
8190

82-
const setTheme = React.useCallback(
83-
value => {
84-
const newTheme = typeof value === 'function' ? value(theme) : value
85-
setThemeState(newTheme)
91+
const setTheme = React.useCallback(value => {
92+
if (typeof value === 'function') {
93+
setThemeState(prevTheme => {
94+
const newTheme = value(prevTheme)
8695

87-
// Save to storage
88-
try {
89-
localStorage.setItem(storageKey, newTheme)
90-
} catch (e) {
91-
// Unsupported
92-
}
93-
},
94-
[theme]
95-
)
96+
saveToLS(storageKey, newTheme)
97+
98+
return newTheme
99+
})
100+
} else {
101+
setThemeState(value)
102+
saveToLS(storageKey, value)
103+
}
104+
}, [])
96105

97106
const handleMediaQuery = React.useCallback(
98107
(e: MediaQueryListEvent | MediaQueryList) => {
@@ -234,7 +243,7 @@ const disableAnimation = (nonce?: string) => {
234243

235244
return () => {
236245
// Force restyle
237-
;(() => window.getComputedStyle(document.body))()
246+
; (() => window.getComputedStyle(document.body))()
238247

239248
// Wait for next tick before removing
240249
setTimeout(() => {

0 commit comments

Comments
 (0)