-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Fix hydration error #643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Fix hydration error #643
Conversation
👷 Deploy request for appcut pending review.Visit the deploys page to approve it
|
|
@SamSyntax is attempting to deploy a commit to the OpenCut OSS Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughIntroduces a hydration-safe CustomThemeProvider that defers applying ThemeProvider until after client mount, integrates it into the app layout, and updates the theme toggle to render icons conditionally based on current theme. Changes
Sequence DiagramsequenceDiagram
participant Browser as Browser (SSR/CSR)
participant Layout as layout.tsx
participant Custom as CustomThemeProvider
participant Theme as ThemeProvider
Browser->>Layout: Initial SSR render (server HTML)
Layout->>Custom: Render CustomThemeProvider with children (mounted=false)
Custom-->>Browser: Return children only (no ThemeProvider) — avoids hydration mismatch
Browser->>Custom: Client mount (useEffect)
Custom->>Custom: set mounted = true
Custom->>Theme: Render ThemeProvider + children (client)
Theme-->>Browser: Theme context applied (post-mount)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Areas needing extra attention:
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/app/layout.tsx (1)
38-58: Refactor component tree to ensureuseTheme()is only called withinThemeProvidercontext.The concern is valid and confirmed.
CustomThemeProviderreturns unwrapped children before mounting (line 16-17 in theme-provider.tsx), butToasterusesuseTheme()and renders unconditionally within that tree. This causesuseTheme()to be called outside its provider context during the initial render, violating hook usage rules and potentially causing runtime warnings or errors.Move
ToasterandAnalyticsinsideStorageProvider, or conditionally render them only after the provider mounts:{mounted && ( <> <Analytics /> <Toaster /> </> )}Alternatively, refactor
CustomThemeProviderto wrap the entire children tree with a loading boundary untilmountedis true.
🧹 Nitpick comments (2)
apps/web/src/components/theme-provider.tsx (2)
6-8: Consider removing redundantchildrendeclaration.
ThemeProviderPropsfromnext-themeslikely already includeschildren: React.ReactNode, making this explicit declaration unnecessary.-interface CustomThemeProviderProps extends ThemeProviderProps { - children: React.ReactNode; -} +type CustomThemeProviderProps = ThemeProviderProps;
20-20: Remove extra whitespace in closing tag.There's an extra space before the closing
>in</ThemeProvider >.- return <ThemeProvider {...props}>{children}</ThemeProvider > + return <ThemeProvider {...props}>{children}</ThemeProvider>
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/src/app/layout.tsx(3 hunks)apps/web/src/components/theme-provider.tsx(1 hunks)apps/web/src/components/theme-toggle.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{jsx,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{jsx,tsx}: Don't useaccessKeyattribute on any HTML element.
Don't setaria-hidden="true"on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Don't use distracting elements like<marquee>or<blink>.
Only use thescopeprop on<th>elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assigntabIndexto non-interactive HTML elements.
Don't use positive integers fortabIndexproperty.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include atitleelement for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible to screen readers.
AssigntabIndexto non-interactive HTML elements witharia-activedescendant.
Include all required ARIA attributes for elements with ARIA roles.
Make sure ARIA properties are valid for the element's supported roles.
Always include atypeattribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden witharia-hidden).
Always include alangattribute on the html element.
Always include atitleattribute for iframe elements.
AccompanyonClickwith at least one of:onKeyUp,onKeyDown, oronKeyPress.
AccompanyonMouseOver/onMouseOutwithonFocus/onBlur.
Include caption tracks for audio and video elements.
Use semantic elements instead of role attributes in JSX.
Make sure all anchors are valid and navigable.
Ensure all ARIA properties (aria-*) are valid.
Use valid, non-abstract ARIA roles for elements with...
Files:
apps/web/src/components/theme-provider.tsxapps/web/src/app/layout.tsxapps/web/src/components/theme-toggle.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Don't use TypeScript enums.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use TypeScript namespaces.
Don't use non-null assertions with the!postfix operator.
Don't use parameter properties in class constructors.
Don't use user-defined types.
Useas constinstead of literal types and type annotations.
Use eitherT[]orArray<T>consistently.
Initialize each enum member value explicitly.
Useexport typefor types.
Useimport typefor types.
Make sure all enum members are literal values.
Don't use TypeScript const enum.
Don't declare empty interfaces.
Don't let variables evolve into any type through reassignments.
Don't use the any type.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use implicit any type on variable declarations.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use the TypeScript directive @ts-ignore.
Use consistent accessibility modifiers on class properties and methods.
Use function types instead of object types with call signatures.
Don't use void type outside of generic or return types.
**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use the TypeScript directive @ts-ignore
Don't use TypeScript enums
Use either T[] or Array consistently
Don't use the any type
Files:
apps/web/src/components/theme-provider.tsxapps/web/src/app/layout.tsxapps/web/src/components/theme-toggle.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{js,jsx,ts,tsx}: Don't use the return value of React.render.
Don't use consecutive spaces in regular expression literals.
Don't use theargumentsobject.
Don't use primitive type aliases or misleading types.
Don't use the comma operator.
Don't write functions that exceed a given Cognitive Complexity score.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use uselessthisaliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names that aren't base 10 or use underscore separators.
Remove redundant terms from logical expressions.
Use while loops instead of...
Files:
apps/web/src/components/theme-provider.tsxapps/web/src/app/layout.tsxapps/web/src/components/theme-toggle.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{tsx,jsx}: Always include a title element for icons unless there's text beside the icon
Always include a type attribute for button elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Don't import React itself
Don't define React components inside other components
Don't use both children and dangerouslySetInnerHTML on the same element
Don't insert comments as text nodes
Use <>...</> instead of ...
Files:
apps/web/src/components/theme-provider.tsxapps/web/src/app/layout.tsxapps/web/src/components/theme-toggle.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{ts,tsx,js,jsx}: Don't use the comma operator
Use for...of statements instead of Array.forEach
Don't initialize variables to undefined
Use .flatMap() instead of map().flat() when possible
Don't assign a value to itself
Avoid unused imports and variables
Don't use await inside loops
Don't hardcode sensitive data like API keys and tokens
Don't use the delete operator
Don't use global eval()
Use String.slice() instead of String.substr() and String.substring()
Don't use else blocks when the if block breaks early
Put default function parameters and optional function parameters last
Use new when throwing an error
Use String.trimStart() and String.trimEnd() over String.trimLeft() and String.trimRight()
Files:
apps/web/src/components/theme-provider.tsxapps/web/src/app/layout.tsxapps/web/src/components/theme-toggle.tsx
🧠 Learnings (4)
📚 Learning: 2025-08-09T09:03:49.797Z
Learnt from: CR
Repo: OpenCut-app/OpenCut PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-09T09:03:49.797Z
Learning: Applies to **/*.{jsx,tsx} : Use semantic elements instead of role attributes in JSX.
Applied to files:
apps/web/src/app/layout.tsx
📚 Learning: 2025-08-15T00:50:09.126Z
Learnt from: CR
Repo: OpenCut-app/OpenCut PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-08-15T00:50:09.126Z
Learning: Applies to **/*.{tsx,jsx} : Use <>...</> instead of <Fragment>...</Fragment>
Applied to files:
apps/web/src/app/layout.tsx
📚 Learning: 2025-08-15T00:50:09.126Z
Learnt from: CR
Repo: OpenCut-app/OpenCut PR: 0
File: .cursor/rules/ultracite.mdc:0-0
Timestamp: 2025-08-15T00:50:09.126Z
Learning: Applies to **/*.{tsx,jsx} : Always include a title element for icons unless there's text beside the icon
Applied to files:
apps/web/src/components/theme-toggle.tsx
📚 Learning: 2025-08-09T09:03:49.797Z
Learnt from: CR
Repo: OpenCut-app/OpenCut PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-09T09:03:49.797Z
Learning: Applies to **/*.{ts,tsx} : Use consistent accessibility modifiers on class properties and methods.
Applied to files:
apps/web/src/components/theme-toggle.tsx
🧬 Code graph analysis (2)
apps/web/src/components/theme-provider.tsx (1)
apps/web/src/components/ui/sonner.tsx (1)
props(8-29)
apps/web/src/app/layout.tsx (1)
apps/web/src/components/theme-provider.tsx (1)
CustomThemeProvider(10-21)
🔇 Additional comments (2)
apps/web/src/app/layout.tsx (1)
12-12: LGTM: Clean import swap for custom wrapper.The import correctly replaces the direct
next-themesThemeProvider with the new hydration-safe wrapper.apps/web/src/components/theme-provider.tsx (1)
1-21: I need to verify a few more things about the actual implementation and what hydration errors are being addressed.Now let me verify what's actually in the layout file:
No issues found. The implementation correctly follows next-themes documentation.
The next-themes library explicitly recommends delaying rendering any theme toggling UI until mounted on the client, and this component implements exactly that pattern. When using useTheme, a hydration mismatch error occurs because values returned by useTheme are undefined on the server, since localStorage cannot be read until mounting on the client.
The mounted check with
useEffectis the standard recommended approach, not a workaround. The component correctly renders children without the ThemeProvider during SSR and wraps them only after client-side hydration completes.
| export default function CustomThemeProvider({ children, ...props }: CustomThemeProviderProps) { | ||
| const [mounted, setMounted] = useState(false); | ||
| useEffect(() => { | ||
| setMounted(true); | ||
| }, []); | ||
|
|
||
| if (!mounted) { | ||
| return <>{children}</>; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Missing ThemeProvider context causes runtime errors for child components.
Returning <>{children}</> without the ThemeProvider wrapper when !mounted (lines 16-18) breaks the theme context. Components like Toaster (which calls useTheme() as shown in the relevant snippet from sonner.tsx) will throw an error during the initial render: "useTheme must be used within a ThemeProvider".
The fix for hydration errors should keep the ThemeProvider wrapped at all times. The suppressHydrationWarning attribute on the <html> tag (already present in layout.tsx line 33) handles theme-related hydration mismatches.
Apply this diff to fix the context issue:
export default function CustomThemeProvider({ children, ...props }: CustomThemeProviderProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
- if (!mounted) {
- return <>{children}</>;
- }
-
- return <ThemeProvider {...props}>{children}</ThemeProvider >
+ return (
+ <ThemeProvider {...props}>
+ {mounted ? children : null}
+ </ThemeProvider>
+ );
}Alternatively, if you want to avoid the layout shift from returning null, simply always render children within ThemeProvider and rely on suppressHydrationWarning:
export default function CustomThemeProvider({ children, ...props }: CustomThemeProviderProps) {
- const [mounted, setMounted] = useState(false);
- useEffect(() => {
- setMounted(true);
- }, []);
-
- if (!mounted) {
- return <>{children}</>;
- }
-
- return <ThemeProvider {...props}>{children}</ThemeProvider >
+ return <ThemeProvider {...props}>{children}</ThemeProvider>;
}The second approach is simpler and leverages Next.js's built-in hydration handling via suppressHydrationWarning.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/src/components/theme-provider.tsx around lines 10 to 18, the
component currently returns <>children</> when !mounted which removes the
ThemeProvider and causes runtime errors (e.g., "useTheme must be used within a
ThemeProvider"); remove the early return and always render the ThemeProvider
wrapper around children (i.e., render ThemeProvider regardless of mounted state)
to preserve theme context during initial render and rely on the existing
suppressHydrationWarning in layout.tsx to handle hydration mismatches.
| {theme === "dark" ? ( | ||
| <Sun className="!size-[1.1rem]" /> | ||
| ) : (<Moon className="!size[1.1rem]" />)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix CSS class typo in Moon icon.
The Moon icon on Line 23 has a syntax error: !size[1.1rem] is missing the hyphen. This will prevent the size utility from applying correctly.
Apply this diff:
- ) : (<Moon className="!size[1.1rem]" />)}
+ ) : (<Moon className="!size-[1.1rem]" />)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {theme === "dark" ? ( | |
| <Sun className="!size-[1.1rem]" /> | |
| ) : (<Moon className="!size[1.1rem]" />)} | |
| {theme === "dark" ? ( | |
| <Sun className="!size-[1.1rem]" /> | |
| ) : (<Moon className="!size-[1.1rem]" />)} |
🤖 Prompt for AI Agents
In apps/web/src/components/theme-toggle.tsx around lines 21 to 23, the Moon
icon's className has a typo `!size[1.1rem]` which prevents the size utility from
applying; change it to `!size-[1.1rem]` so the class reads
className="!size-[1.1rem]" (matching the Sun icon) to restore correct sizing.
Fix typo in classname
Description
Please include a summary of the changes and the related issue. Please also include relevant motivation and context.
Fixes #629 #635 #621
This is a really tiny modification that fixes hydration error mentioned in #629, #635 and #621
CustomThemeProvider just wraps the ThemeProvider from next-themes and uses useState to set mounted state as false initially and then calls useEffect that runs only once on the initial load to set mounted to true. Since it's a client-side hook, it does not run on server so we receive a neutral html from server without theme mismatch. I believe this is a common pattern in working with next-themes, but if anyone knows a better way to handle that I would love to know!
This pr also adds the theme icon change on toggle.
Type of change
How Has This Been Tested?
This change has been tested on on both Chromium browser and Zen browser which is using firefox engine under the hood.
Test Configuration:
Screenshots (if applicable)
Checklist:
Summary by CodeRabbit
New Features
Bug Fixes
Style