Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/components/providers/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { ThemeProvider as NextThemesProvider } from 'next-themes';
import { ToastContainer } from 'react-toastify';
import { ThemedToastContainer } from '@/components/ui/themed-toast-container';
import { TooltipProvider } from '@/components/ui/tooltip';

export function ThemeProviders({ children }: { children: React.ReactNode }) {
Expand All @@ -14,11 +14,7 @@ export function ThemeProviders({ children }: { children: React.ReactNode }) {
themes={['light', 'dark']}
>
<TooltipProvider delayDuration={200}>{children}</TooltipProvider>
<ToastContainer
position="bottom-right"
toastClassName="bg-[#fff] dark:bg-[#202426] text-[#000] dark:text-[#fff]"
toastStyle={{ borderRadius: '3px', fontSize: '16px' }}
/>
<ThemedToastContainer />
</NextThemesProvider>
);
}
17 changes: 17 additions & 0 deletions src/components/ui/themed-toast-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import { useTheme } from 'next-themes';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';

export function ThemedToastContainer() {
const { resolvedTheme } = useTheme();

return (
<ToastContainer
position="bottom-right"
theme={resolvedTheme === 'dark' ? 'dark' : 'light'}
toastStyle={{ borderRadius: '3px', fontSize: '16px' }}
/>
);
}
Comment on lines +7 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -nP '\bresolvedTheme\b' --type=ts --type=tsx -C3

Repository: antoncoding/monarch

Length of output: 90


🏁 Script executed:

# Also check for suppressHydrationWarning or other hydration handling patterns
rg -i 'suppressHydrationWarning|hydration' --type=ts --type=tsx | head -20

Repository: antoncoding/monarch

Length of output: 90


🌐 Web query:

next-themes resolvedTheme undefined hydration initial render

💡 Result:

Short answer

  • resolvedTheme is often undefined on initial render because the server can't access browser state (localStorage / prefers-color-scheme). next-themes returns undefined until the component mounts on the client, which causes hydration mismatches. [1][2]

How to fix (pick one)

  1. Wait for client mount: render theme-dependent UI only after mounting.
    • const [mounted, setMounted] = useState(false);
    • useEffect(() => setMounted(true), []);
    • if (!mounted) return null (or a neutral placeholder). [1][2]
  2. Client-only component: lazy-load the theme-dependent component with next/dynamic({ ssr: false }). [1][4]
  3. Rely on ThemeProvider's no-flash script: ensure ThemeProvider is configured (attribute/class, enableColorScheme, storageKey) so it injects the pre-paint script that sets the HTML attribute and avoids flashing; still avoid reading resolvedTheme on server. [1][2]
  4. For SSR-synced solution: determine theme on the server (cookie or server-side logic) and render that on initial HTML so server and client match. [5]

Notes

  • These fixes address hydration errors documented by next-themes and Next.js. If you want, tell me which approach you prefer and share the relevant code and I’ll give a patched snippet. [1][2][3]

Sources
[1] next-themes README / examples (avoiding hydration mismatch, resolvedTheme examples).
[2] next-themes GitHub (themed images / README discussion).
[3] Next.js docs on hydration errors and fixes.
[4] next-themes README: example using next/dynamic({ ssr: false }).
[5] community posts / articles on SSR approaches to theming.


🏁 Script executed:

rg '\bresolvedTheme\b' --type=typescript -C3

Repository: antoncoding/monarch

Length of output: 97


🏁 Script executed:

# Also check for useTheme hook usage patterns
rg 'useTheme\(\)' --type=typescript -C2

Repository: antoncoding/monarch

Length of output: 97


🏁 Script executed:

# Check if there's a ThemeProvider setup in the repo
fd -e ts -e tsx | head -20 && echo "---" && git ls-files | grep -E '\.(ts|tsx)$' | head -20

Repository: antoncoding/monarch

Length of output: 1102


🏁 Script executed:

# Look for the _app or layout file to see ThemeProvider configuration
find . -name '_app.*' -o -name 'layout.*' -o -name 'app.*' | grep -E '\.(ts|tsx)$' | head -10

Repository: antoncoding/monarch

Length of output: 79


🏁 Script executed:

# Use basic grep approach instead
grep -r "resolvedTheme" --include="*.ts" --include="*.tsx" -n

Repository: antoncoding/monarch

Length of output: 483


🏁 Script executed:

# Check the layout.tsx and ThemedToastContainer directly
cat app/layout.tsx

Repository: antoncoding/monarch

Length of output: 1949


🏁 Script executed:

cat src/components/ui/themed-toast-container.tsx

Repository: antoncoding/monarch

Length of output: 491


🏁 Script executed:

cat src/components/providers/ThemeProvider.tsx

Repository: antoncoding/monarch

Length of output: 674


Handle resolvedTheme hydration to prevent theme flash on initial load.

When the component first renders on the client, resolvedTheme is undefined, which defaults this to the light theme. Dark mode users will see a brief light theme flash before hydration completes. Wrap the component to only render after the client has mounted, or use next/dynamic({ ssr: false }) to skip server rendering entirely.

🤖 Prompt for AI Agents
In @src/components/ui/themed-toast-container.tsx around lines 7 - 17, The
ThemedToastContainer currently reads resolvedTheme from useTheme on first render
causing a light-theme flash; update ThemedToastContainer to avoid rendering
until the client has mounted (e.g., track a local mounted state and only return
the <ToastContainer> when mounted) or export it via next/dynamic with ssr: false
so it only renders on the client; ensure you reference resolvedTheme from
useTheme and pass theme to ToastContainer only after mounted to prevent the
initial undefined -> light fallback.

16 changes: 7 additions & 9 deletions src/features/positions/components/from-markets-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import { previewMarketState } from '@/utils/morpho';
import { convertApyToApr } from '@/utils/rateMath';
import type { MarketPosition } from '@/utils/types';

type PositionWithPendingDelta = MarketPosition & { pendingDelta: number };
type PositionWithPendingDelta = MarketPosition & { pendingDelta: bigint };

type FromMarketsTableProps = {
positions: PositionWithPendingDelta[];
selectedMarketUniqueKey: string;
onSelectMarket: (marketUniqueKey: string) => void;
onSelectMax?: (marketUniqueKey: string, amount: number) => void;
onSelectMax?: (marketUniqueKey: string, amount: bigint) => void;
};

const PER_PAGE = 5;
Expand All @@ -30,11 +30,10 @@ export function FromMarketsTable({ positions, selectedMarketUniqueKey, onSelectM
const paginatedPositions = positions.slice((currentPage - 1) * PER_PAGE, currentPage * PER_PAGE);

const getApyPreview = (position: PositionWithPendingDelta) => {
if (position.pendingDelta === 0) return null;
if (position.pendingDelta === 0n) return null;

try {
const deltaBigInt = BigInt(Math.floor(position.pendingDelta));
return previewMarketState(position.market, deltaBigInt, undefined);
return previewMarketState(position.market, position.pendingDelta, undefined);
} catch {
return null;
}
Expand Down Expand Up @@ -67,11 +66,10 @@ export function FromMarketsTable({ positions, selectedMarketUniqueKey, onSelectM
<TableBody>
{paginatedPositions.map((position) => {
const userConfirmedSupply = BigInt(position.state.supplyAssets);
const pendingDeltaBigInt = BigInt(position.pendingDelta);
const userNetSupply = userConfirmedSupply + pendingDeltaBigInt;
const userNetSupply = userConfirmedSupply + position.pendingDelta;

const rawMarketLiquidity = BigInt(position.market.state.liquidityAssets);
const adjustedMarketLiquidity = rawMarketLiquidity + pendingDeltaBigInt;
const adjustedMarketLiquidity = rawMarketLiquidity + position.pendingDelta;

const maxTransferableAmount = userNetSupply < adjustedMarketLiquidity ? userNetSupply : adjustedMarketLiquidity;

Expand Down Expand Up @@ -154,7 +152,7 @@ export function FromMarketsTable({ positions, selectedMarketUniqueKey, onSelectM
e.stopPropagation();
onSelectMarket(position.market.uniqueKey);
if (onSelectMax && maxTransferableAmount > 0n) {
onSelectMax(position.market.uniqueKey, Number(maxTransferableAmount));
onSelectMax(position.market.uniqueKey, maxTransferableAmount);
}
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
import { MarketIdentity, MarketIdentityMode } from '@/features/markets/components/market-identity';
import { TokenIcon } from '@/components/shared/token-icon';
import { useRateLabel } from '@/hooks/useRateLabel';
import { formatReadable } from '@/utils/balance';
import { previewMarketState } from '@/utils/morpho';
import type { GroupedPosition, Market } from '@/utils/types';
import { ApyPreview } from '../preview/apy-preview';
Expand Down Expand Up @@ -75,7 +76,9 @@ export function RebalanceActionRow({
}, [toMarket, amount, groupedPosition.loanAssetDecimals]);

// Format amount for display
const displayAmount = typeof amount === 'string' ? amount : formatUnits(amount, groupedPosition.loanAssetDecimals);
const rawAmount = typeof amount === 'string' ? amount : formatUnits(amount, groupedPosition.loanAssetDecimals);
// In display mode (bigint), format nicely; in input mode (string), show raw value for editing
const displayAmount = typeof amount === 'string' ? rawAmount : formatReadable(rawAmount, 4);

return (
<div className="flex items-center gap-4">
Expand Down
22 changes: 11 additions & 11 deletions src/features/positions/components/rebalance/rebalance-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,16 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
}, [markets, groupedPosition.loanAssetAddress, groupedPosition.chainId]);

const getPendingDelta = useCallback(
(marketUniqueKey: string) => {
return rebalanceActions.reduce((acc: number, action: RebalanceAction) => {
(marketUniqueKey: string): bigint => {
return rebalanceActions.reduce((acc: bigint, action: RebalanceAction) => {
if (action.fromMarket.uniqueKey === marketUniqueKey) {
return acc - Number(action.amount);
return acc - BigInt(action.amount);
}
if (action.toMarket.uniqueKey === marketUniqueKey) {
return acc + Number(action.amount);
return acc + BigInt(action.amount);
}
return acc;
}, 0);
}, 0n);
},
[rebalanceActions],
);
Expand Down Expand Up @@ -107,7 +107,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
const oldBalance = groupedPosition.markets.find((m) => m.market.uniqueKey === selectedFromMarketUniqueKey)?.state.supplyAssets;

const pendingDelta = getPendingDelta(selectedFromMarketUniqueKey);
const pendingBalance = BigInt(oldBalance ?? 0) + BigInt(pendingDelta);
const pendingBalance = BigInt(oldBalance ?? 0) + pendingDelta;

const scaledAmount = parseUnits(amount, groupedPosition.loanAssetDecimals);
if (scaledAmount > pendingBalance) {
Expand Down Expand Up @@ -147,13 +147,13 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
}, []);

const handleMaxSelect = useCallback(
(marketUniqueKey: string, maxAmount: number) => {
(marketUniqueKey: string, maxAmount: bigint) => {
const market = eligibleMarkets.find((m) => m.uniqueKey === marketUniqueKey);
if (!market) return;

setSelectedFromMarketUniqueKey(marketUniqueKey);
// Convert the amount to a string with the correct number of decimals
const formattedAmount = formatUnits(BigInt(Math.floor(maxAmount)), groupedPosition.loanAssetDecimals);
// Convert the bigint amount to a string with the correct number of decimals
const formattedAmount = formatUnits(maxAmount, groupedPosition.loanAssetDecimals);
setAmount(formattedAmount);
},
[eligibleMarkets, groupedPosition.loanAssetDecimals],
Expand All @@ -175,11 +175,11 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch,
const selectedPosition = groupedPosition.markets.find((p) => p.market.uniqueKey === selectedFromMarketUniqueKey);

// Get the pending delta for this market
const pendingDelta = selectedPosition ? getPendingDelta(selectedPosition.market.uniqueKey) : 0;
const pendingDelta = selectedPosition ? getPendingDelta(selectedPosition.market.uniqueKey) : 0n;

// Check if this is a max amount considering pending delta
const isMaxAmount =
selectedPosition !== undefined && BigInt(selectedPosition.state.supplyAssets) + BigInt(pendingDelta) === scaledAmount;
selectedPosition !== undefined && BigInt(selectedPosition.state.supplyAssets) + pendingDelta === scaledAmount;

// Create the action using the helper function
const action = createAction(fromMarket, toMarket, scaledAmount, isMaxAmount);
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useStyledToast.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback } from 'react';
import { toast, type ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { StyledToast } from '../components/ui/styled-toast';

export function useStyledToast() {
Expand Down
1 change: 0 additions & 1 deletion src/hooks/useTransactionWithToast.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef } from 'react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi';
import { StyledToast, TransactionToast } from '@/components/ui/styled-toast';
import { getExplorerTxURL } from '../utils/external';
Expand Down
5 changes: 4 additions & 1 deletion src/utils/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,10 @@ const supportedTokens = [
symbol: 'sDAI',
img: require('../imgs/tokens/sdai.svg') as string,
decimals: 18,
networks: [{ chain: mainnet, address: '0x83F20F44975D03b1b09e64809B757c47f942BEeA' }],
networks: [
{ chain: mainnet, address: '0x83F20F44975D03b1b09e64809B757c47f942BEeA' },
{ chain: base, address: '0x99ac4484e8a1dbd6a185380b3a811913ac884d87' },
],
Comment thread
antoncoding marked this conversation as resolved.
},
{
symbol: 'wstETH',
Expand Down