Skip to content

Commit

Permalink
Prevent Cloudflare Rocket Loader deferring script (#156)
Browse files Browse the repository at this point in the history
* Prevent Cloudflare Rocket Loader deferring script

* Add scriptAttribute prop to ThemeProvider

* Wording

* fix types, add test

---------

Co-authored-by: Paco <[email protected]>
  • Loading branch information
andreacassani and pacocoursey authored Nov 4, 2024
1 parent 23c00a8 commit 5287ede
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 5 deletions.
9 changes: 9 additions & 0 deletions next-themes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ All your theme configuration is passed to ThemeProvider.
- `value`: Optional mapping of theme name to attribute value
- value is an `object` where key is the theme name and value is the attribute value ([example](#differing-dom-attribute-and-theme-name))
- `nonce`: Optional nonce passed to the injected `script` tag, used to allow-list the next-themes script in your CSP
- `scriptProps`: Optional props to pass to the injected `script` tag ([example](#using-with-cloudflare-rocket-loader))

### useTheme

Expand Down Expand Up @@ -267,6 +268,14 @@ document.documentElement.getAttribute('data-theme')
// => "my-pink-theme"
```

### Using with Cloudflare Rocket Loader

[Rocket Loader](https://developers.cloudflare.com/fundamentals/speed/rocket-loader/) is a Cloudflare optimization that defers the loading of inline and external scripts to prioritize the website content. Since next-themes relies on a script injection to avoid screen flashing on page load, Rocket Loader breaks this functionality. Individual scripts [can be ignored](https://developers.cloudflare.com/fundamentals/speed/rocket-loader/ignore-javascripts/) by adding the `data-cfasync="false"` attribute to the script tag:

```jsx
<ThemeProvider scriptProps={{ 'data-cfasync': 'false' }}>
```

### More than light and dark mode

next-themes is designed to support any number of themes! Simply pass a list of themes:
Expand Down
14 changes: 14 additions & 0 deletions next-themes/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,17 @@ describe('setTheme', () => {
expect(result.current.resolvedTheme).toBe('light')
})
})

describe('inline script', () => {
test('should pass props to script', () => {
act(() => {
render(
<ThemeProvider defaultTheme="light" scriptProps={{ 'data-test': '1234' }}>
<HelperComponent />
</ThemeProvider>
)
})

expect(document.querySelector('script[data-test="1234"]')).toBeTruthy()
})
})
10 changes: 7 additions & 3 deletions next-themes/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const Theme = ({
attribute = 'data-theme',
value,
children,
nonce
nonce,
scriptProps
}: ThemeProviderProps) => {
const [theme, setThemeState] = React.useState(() => getTheme(storageKey, defaultTheme))
const [resolvedTheme, setResolvedTheme] = React.useState(() => getTheme(storageKey))
Expand Down Expand Up @@ -161,7 +162,8 @@ const Theme = ({
defaultTheme,
value,
themes,
nonce
nonce,
scriptProps
}}
/>

Expand All @@ -180,7 +182,8 @@ const ThemeScript = React.memo(
defaultTheme,
value,
themes,
nonce
nonce,
scriptProps
}: Omit<ThemeProviderProps, 'children'> & { defaultTheme: string }) => {
const scriptArgs = JSON.stringify([
attribute,
Expand All @@ -195,6 +198,7 @@ const ThemeScript = React.memo(

return (
<script
{...scriptProps}
suppressHydrationWarning
nonce={typeof window === 'undefined' ? nonce : ''}
dangerouslySetInnerHTML={{ __html: `(${script.toString()})(${scriptArgs})` }}
Expand Down
16 changes: 14 additions & 2 deletions next-themes/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ interface ValueObject {
[themeName: string]: string
}

type DataAttribute = `data-${string}`

interface ScriptProps
extends React.DetailedHTMLProps<
React.ScriptHTMLAttributes<HTMLScriptElement>,
HTMLScriptElement
> {
[dataAttribute: DataAttribute]: any
}

export interface UseThemeProps {
/** List of all available theme names */
themes: string[]
Expand All @@ -19,7 +29,7 @@ export interface UseThemeProps {
systemTheme?: 'dark' | 'light' | undefined
}

export type Attribute = `data-${string}` | 'class'
export type Attribute = DataAttribute | 'class'

export interface ThemeProviderProps extends React.PropsWithChildren {
/** List of all available theme names */
Expand All @@ -41,5 +51,7 @@ export interface ThemeProviderProps extends React.PropsWithChildren {
/** Mapping of theme name to HTML attribute value. Object where key is the theme name and value is the attribute value */
value?: ValueObject | undefined
/** Nonce string to pass to the inline script for CSP headers */
nonce?: string | undefined
nonce?: string
/** Props to pass the inline script */
scriptProps?: ScriptProps
}

0 comments on commit 5287ede

Please sign in to comment.