From 57c299833d916d585d9c81d9fc5f9dbbb6d0940b Mon Sep 17 00:00:00 2001 From: Alexandru Bereghici Date: Fri, 2 Feb 2024 15:11:41 +0200 Subject: [PATCH] Extract transition code in a separate hook. Add documentation. --- README.md | 2 + packages/remix-themes-app/app/root.tsx | 6 ++- .../remix-themes-app/app/styles/index.css | 4 ++ packages/remix-themes/src/theme-provider.tsx | 50 +++++++------------ .../src/useCorrectCssTransition.ts | 43 ++++++++++++++++ yarn.lock | 4 +- 6 files changed, 75 insertions(+), 34 deletions(-) create mode 100644 packages/remix-themes/src/useCorrectCssTransition.ts diff --git a/README.md b/README.md index c7cfb99..9f6db02 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,8 @@ Let's dig into the details. - `specifiedTheme`: The theme from the session storage. - `themeAction`: The action name used to change the theme in the session storage. +- `disableTransitionOnThemeChange`: Disable CSS transitions on theme change to + prevent the flashing effect. ### useTheme diff --git a/packages/remix-themes-app/app/root.tsx b/packages/remix-themes-app/app/root.tsx index df9c49a..94fedc4 100644 --- a/packages/remix-themes-app/app/root.tsx +++ b/packages/remix-themes-app/app/root.tsx @@ -57,7 +57,11 @@ function App() { export default function AppWithProviders() { const data = useLoaderData() return ( - + ) diff --git a/packages/remix-themes-app/app/styles/index.css b/packages/remix-themes-app/app/styles/index.css index 9492b5b..4dd0662 100644 --- a/packages/remix-themes-app/app/styles/index.css +++ b/packages/remix-themes-app/app/styles/index.css @@ -1,19 +1,23 @@ [data-theme='dark'] { background-color: #000; color: white; + transition: background-color 2s ease; } [data-theme='light'] { background-color: #f2f2f2; color: black; + transition: background-color 2s ease; } .dark { background-color: #000; color: white; + transition: background-color 2s ease; } .light { background-color: #f2f2f2; color: black; + transition: background-color 2s ease; } diff --git a/packages/remix-themes/src/theme-provider.tsx b/packages/remix-themes/src/theme-provider.tsx index b17a248..319e0bf 100644 --- a/packages/remix-themes/src/theme-provider.tsx +++ b/packages/remix-themes/src/theme-provider.tsx @@ -1,6 +1,7 @@ import type {Dispatch, ReactNode, SetStateAction} from 'react' import {createContext, useState, useContext, useEffect, useRef} from 'react' import {useBroadcastChannel} from './useBroadcastChannel' +import {useCorrectCssTransition} from './useCorrectCssTransition' export enum Theme { DARK = 'dark', @@ -15,6 +16,7 @@ const ThemeContext = createContext(undefined) ThemeContext.displayName = 'ThemeContext' const prefersLightMQ = '(prefers-color-scheme: light)' + const getPreferredTheme = () => window.matchMedia(prefersLightMQ).matches ? Theme.LIGHT : Theme.DARK @@ -34,6 +36,10 @@ export function ThemeProvider({ themeAction, disableTransitionOnThemeChange = false, }: ThemeProviderProps) { + const ensureCorrectTransition = useCorrectCssTransition({ + disableTransitions: disableTransitionOnThemeChange, + }) + const [theme, setTheme] = useState(() => { // On the server, if we don't have a specified theme then we should // return null and the clientThemeCode will set the theme for us @@ -52,31 +58,11 @@ export function ThemeProvider({ const mountRun = useRef(false) - const broadcastThemeChange = useBroadcastChannel('remix-themes', e => - setTheme(e.data), - ) - - const disableTransition = () => { - const style = document.createElement('style') - - style.textContent = ` - * { - -ms-transition: none!important; - -webkit-transition: none!important; - -moz-transition: none!important; - -o-transition: none!important; - transition: none!important - } - ` - - document.head.appendChild(style); - - (() => window.getComputedStyle(document.body))(); - - setTimeout(() => { - document.head.removeChild(style); - }, 1); - } + const broadcastThemeChange = useBroadcastChannel('remix-themes', e => { + ensureCorrectTransition(() => { + setTheme(e.data) + }) + }) useEffect(() => { if (!mountRun.current) { @@ -90,18 +76,20 @@ export function ThemeProvider({ body: JSON.stringify({theme}), }) - broadcastThemeChange(theme) - disableTransitionOnThemeChange && disableTransition() - }, [broadcastThemeChange, disableTransitionOnThemeChange, theme, themeAction]) + ensureCorrectTransition(() => { + broadcastThemeChange(theme) + }) + }, [broadcastThemeChange, theme, themeAction, ensureCorrectTransition]) useEffect(() => { const handleChange = (ev: MediaQueryListEvent) => { - setTheme(ev.matches ? Theme.LIGHT : Theme.DARK) - disableTransitionOnThemeChange && disableTransition() + ensureCorrectTransition(() => { + setTheme(ev.matches ? Theme.LIGHT : Theme.DARK) + }) } mediaQuery?.addEventListener('change', handleChange) return () => mediaQuery?.removeEventListener('change', handleChange) - }, []) + }, [ensureCorrectTransition]) return ( diff --git a/packages/remix-themes/src/useCorrectCssTransition.ts b/packages/remix-themes/src/useCorrectCssTransition.ts new file mode 100644 index 0000000..b2765a3 --- /dev/null +++ b/packages/remix-themes/src/useCorrectCssTransition.ts @@ -0,0 +1,43 @@ +import {useCallback} from 'react' + +function withoutTransition(callback: Function) { + const css = document.createElement('style') + css.appendChild( + document.createTextNode( + `* { + -webkit-transition: none !important; + -moz-transition: none !important; + -o-transition: none !important; + -ms-transition: none !important; + transition: none !important; + }`, + ), + ) + document.head.appendChild(css) + + callback() + + setTimeout(() => { + // Calling getComputedStyle forces the browser to redraw + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _ = window.getComputedStyle(css).transition + document.head.removeChild(css) + }, 0) +} + +export function useCorrectCssTransition({ + disableTransitions = false, +}: {disableTransitions?: boolean} = {}) { + return useCallback( + (callback: Function) => { + if (disableTransitions) { + withoutTransition(() => { + callback() + }) + } else { + callback() + } + }, + [disableTransitions], + ) +} diff --git a/yarn.lock b/yarn.lock index e1ebdd0..03a7584 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1514,7 +1514,7 @@ source-map-support "^0.5.21" stream-slice "^0.1.2" -"@remix-run/react@latest": +"@remix-run/react@1.12.0", "@remix-run/react@latest": version "1.12.0" resolved "https://registry.yarnpkg.com/@remix-run/react/-/react-1.12.0.tgz#7ad27e7d152b0980ef09ac851f849cc445a9e51f" integrity sha512-BokbMOILGJvUvwOsTXAUrucvfFz/SBKVgSHke2+89DZIU7H2Z1UWe5t8wRTfjQnfnSH2tAXCF0QxmVpo1GQ3dg== @@ -1538,7 +1538,7 @@ express "^4.17.1" morgan "^1.10.0" -"@remix-run/server-runtime@1.12.0", "@remix-run/server-runtime@latest": +"@remix-run/server-runtime@1.12.0": version "1.12.0" resolved "https://registry.yarnpkg.com/@remix-run/server-runtime/-/server-runtime-1.12.0.tgz#a2072c2e88b948b2a657993574cad01d42cf8cd4" integrity sha512-7I0165Ns/ffPfCEfuiqD58lMderTn2s/sew1xJ34ONa21mG/7+5T7diHIgxKST8rS3816JPmlwSqUaHgwbmO6Q==