Skip to content

Conversation

@SamSyntax
Copy link
Contributor

@SamSyntax SamSyntax commented Nov 1, 2025

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

  • Bug fix (non-breaking change which fixes an issue)
  • Code refactoring

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:

  • Node version: v23.11.1
  • Browser (if applicable): Chromium, Zen
  • Operating System: Arch Linux

Screenshots (if applicable)

image

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have added screenshots if ui has been changed
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Summary by CodeRabbit

  • New Features

    • Adds a hydration-safe custom theme provider to improve theming reliability on initial load.
  • Bug Fixes

    • Prevents hydration mismatches by deferring theme application until after mount.
  • Style

    • Theme toggle styling corrected.
    • Theme toggle now displays contextual icons (sun in dark mode, moon in light mode).

@netlify
Copy link

netlify bot commented Nov 1, 2025

👷 Deploy request for appcut pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit e78e9e3

@vercel
Copy link

vercel bot commented Nov 1, 2025

@SamSyntax is attempting to deploy a commit to the OpenCut OSS Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 1, 2025

Walkthrough

Introduces 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

Cohort / File(s) Summary
Layout Integration
apps/web/src/app/layout.tsx
Replaced ThemeProvider import/usage with CustomThemeProvider; props passed unchanged.
Theme Provider Implementation
apps/web/src/components/theme-provider.tsx
New CustomThemeProvider component and CustomThemeProviderProps interface; uses useEffect to set mounted and defers rendering ThemeProvider until mounted to avoid hydration mismatches.
Theme Toggle UI
apps/web/src/components/theme-toggle.tsx
Updated icon logic to render Sun when theme is "dark" and Moon otherwise; corrected a class-name typo by removing an extra dash (changed !size-[1.1rem]!size[1.1rem]).

Sequence Diagram

sequenceDiagram
    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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Areas needing extra attention:

  • Verify the class-name change in theme-toggle.tsx matches the project's utility syntax and intent.
  • Confirm CustomThemeProvider useEffect dependency and mounting logic are correct and won't cause unnecessary re-renders.
  • Check for any visual flicker or delayed theme application on first paint due to deferring ThemeProvider.

Poem

🐰 I hopped in code where themes may clash,
Waiting to mount before the flash.
Custom wrap keeps SSR in tune,
Sun or Moon now sings by moon. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Fix hydration error" is short, concise, and directly reflects the primary objective of the changeset. The main modification involves introducing a CustomThemeProvider component to defer theme application until client-side mounting, preventing server-client hydration mismatches. This change is clearly indicated by the title, which accurately summarizes the key problem being addressed without unnecessary verbosity or vague language.
Linked Issues Check ✅ Passed The PR addresses the hydration error issue described in linked issue #629 through introducing a CustomThemeProvider component that defers theme application until client-side mounting, a standard pattern for preventing hydration mismatches in Next.js applications. The CustomThemeProvider uses useState and useEffect to ensure theme rendering only occurs on the client after hydration completes, directly solving the server-client rendering mismatch that causes hydration errors. The PR also implements the additional theme icon toggle enhancement as mentioned in the description, maintaining alignment with the stated objectives.
Out of Scope Changes Check ✅ Passed All changes in the PR are directly within scope of the stated objectives. The new CustomThemeProvider component in theme-provider.tsx addresses the hydration error fix requirement, the replacement in layout.tsx is necessary to apply this fix, and the modifications to theme-toggle.tsx implement the theme icon change feature explicitly mentioned in the PR description. The minor typo fix in the icon size utility class is a reasonable accompanying improvement to the theme toggle changes and does not constitute out-of-scope work.
Description Check ✅ Passed The PR description follows the provided template structure with all major sections properly completed. It includes a clear summary referencing the linked issues (#629, #635, #621), a detailed explanation of the CustomThemeProvider pattern and how it prevents hydration mismatches, the type of changes marked (Bug fix and Code refactoring), comprehensive testing information with specific browser, Node version, and OS details, and a screenshot demonstrating the UI change. While some checklist items remain unchecked (comments, documentation, tests), the core required sections are well-filled with meaningful content.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b457045 and e78e9e3.

📒 Files selected for processing (1)
  • apps/web/src/components/theme-toggle.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/theme-toggle.tsx

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@SamSyntax SamSyntax marked this pull request as ready for review November 1, 2025 13:03
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 ensure useTheme() is only called within ThemeProvider context.

The concern is valid and confirmed. CustomThemeProvider returns unwrapped children before mounting (line 16-17 in theme-provider.tsx), but Toaster uses useTheme() and renders unconditionally within that tree. This causes useTheme() to be called outside its provider context during the initial render, violating hook usage rules and potentially causing runtime warnings or errors.

Move Toaster and Analytics inside StorageProvider, or conditionally render them only after the provider mounts:

{mounted && (
  <>
    <Analytics />
    <Toaster />
  </>
)}

Alternatively, refactor CustomThemeProvider to wrap the entire children tree with a loading boundary until mounted is true.

🧹 Nitpick comments (2)
apps/web/src/components/theme-provider.tsx (2)

6-8: Consider removing redundant children declaration.

ThemeProviderProps from next-themes likely already includes children: 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

📥 Commits

Reviewing files that changed from the base of the PR and between bee7aba and b457045.

📒 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 use accessKey attribute on any HTML element.
Don't set aria-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 the scope prop 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 assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
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 a title element 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.
Assign tabIndex to non-interactive HTML elements with aria-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 a type attribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden).
Always include a lang attribute on the html element.
Always include a title attribute for iframe elements.
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress.
Accompany onMouseOver/onMouseOut with onFocus/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.tsx
  • apps/web/src/app/layout.tsx
  • apps/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.
Use as const instead of literal types and type annotations.
Use either T[] or Array<T> consistently.
Initialize each enum member value explicitly.
Use export type for types.
Use import type for 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.tsx
  • apps/web/src/app/layout.tsx
  • apps/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 the arguments object.
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 useless this aliasing.
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.tsx
  • apps/web/src/app/layout.tsx
  • apps/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.tsx
  • apps/web/src/app/layout.tsx
  • apps/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.tsx
  • apps/web/src/app/layout.tsx
  • apps/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-themes ThemeProvider 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 useEffect is 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.

Comment on lines +10 to +18
export default function CustomThemeProvider({ children, ...props }: CustomThemeProviderProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return <>{children}</>;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines 21 to 23
{theme === "dark" ? (
<Sun className="!size-[1.1rem]" />
) : (<Moon className="!size[1.1rem]" />)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
{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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] can't access property "getDirectory", navigator.storage is undefined

1 participant