@@ -60,6 +60,15 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
6060 // Snapshot and Ethereum proposals are read-only (no voting)
6161 const isReadOnly = isSnapshot || isEthereum ;
6262
63+ // Mount flag keeps the initial client render identical to the SSR tree.
64+ // `address` (thirdweb, synchronous from storage) and `propdates` (async)
65+ // are undefined on the server, so we must treat them as undefined during
66+ // the first hydration pass too, then let post-mount updates reveal tabs.
67+ // The <Tabs> shell is always rendered, so this only affects which
68+ // <TabsTrigger>s appear — no structural mismatch either way.
69+ const [ mounted , setMounted ] = useState ( false ) ;
70+ useEffect ( ( ) => setMounted ( true ) , [ ] ) ;
71+
6372 const [ userVote , setUserVote ] = useState < "FOR" | "AGAINST" | "ABSTAIN" | null > ( null ) ;
6473 const [ userVoteReason , setUserVoteReason ] = useState < string | null > ( null ) ;
6574 const [ hasRecentVoteConfirmation , setHasRecentVoteConfirmation ] = useState ( false ) ;
@@ -189,10 +198,10 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
189198
190199 // Show propdates tab if there's at least one propdate OR the connected user is the proposal owner
191200 const isProposalOwner =
192- address && proposal . proposer
201+ mounted && address && proposal . proposer
193202 ? address . toLowerCase ( ) === proposal . proposer . toLowerCase ( )
194203 : false ;
195- const hasPropdates = ( propdates ?. length ?? 0 ) > 0 ;
204+ const hasPropdates = mounted && ( propdates ?. length ?? 0 ) > 0 ;
196205 const shouldShowPropdatesTab = hasPropdates || isProposalOwner ;
197206
198207 // Show votes tab if there are any votes (for any status: Active, Executed, Defeated, etc.)
@@ -206,9 +215,11 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
206215 proposal . targets &&
207216 proposal . targets . length > 0 ;
208217
209- // Count visible tabs to determine if we should show tabs at all
218+ // Count visible tabs to decide whether to render the TabsList.
219+ // The <Tabs> shell itself is always rendered so the SSR tree is stable
220+ // and does not depend on client-only state (address, fetched propdates).
210221 const visibleTabsCount = 1 + ( shouldShowVotesTab ? 1 : 0 ) + ( shouldShowPropdatesTab ? 1 : 0 ) ;
211- const shouldShowTabs = visibleTabsCount > 1 ;
222+ const shouldShowTabsList = visibleTabsCount > 1 ;
212223
213224 // Show voting card for active proposals (connection check moved to VotingControls to avoid hydration issues)
214225 // Hide voting for read-only proposals (Snapshot and Ethereum)
@@ -277,8 +288,8 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
277288 { isProposalSuccessful ( proposal . status ) && (
278289 < ProposalActions proposal = { proposal } onActionSuccess = { handleActionSuccess } />
279290 ) }
280- { shouldShowTabs ? (
281- < Tabs defaultValue = "details" className = "w-full" >
291+ < Tabs defaultValue = "details" className = "w-full" >
292+ { shouldShowTabsList && (
282293 < div className = "overflow-x-auto" >
283294 < TabsList
284295 className = { `grid w-full ${ visibleTabsCount === 2 ? "grid-cols-2" : "grid-cols-3" } min-w-fit` }
@@ -288,52 +299,11 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
288299 { shouldShowPropdatesTab && < TabsTrigger value = "propdates" > Propdates</ TabsTrigger > }
289300 </ TabsList >
290301 </ div >
291- < TabsContent value = "details" className = "space-y-6 mt-6" >
292- < ProposalDescriptionCard description = { proposal . description } />
293- { hasTransactionData && (
294- < Card >
295- < CardHeader >
296- < CardTitle > Proposed Transactions</ CardTitle >
297- </ CardHeader >
298- < CardContent >
299- < ProposalTransactionVisualization
300- targets = { proposal . targets }
301- values = { proposal . values }
302- signatures = { proposal . signatures }
303- calldatas = { proposal . calldatas }
304- descriptions = { ( proposal as MultiChainProposal ) . txDescriptions }
305- />
306- </ CardContent >
307- </ Card >
308- ) }
309- </ TabsContent >
310- { shouldShowVotesTab && (
311- < TabsContent value = "votes" className = "mt-6 space-y-6" >
312- < ProposalVotesList
313- votes = { votesList . map ( ( v ) => ( {
314- voter : v . voter ,
315- choice : v . choice ,
316- votes : v . votes ,
317- reason : ( v as { reason ?: string | null } ) . reason ?? null ,
318- timestamp : ( v as { timestamp ?: number } ) . timestamp ,
319- } ) ) }
320- proposalId = { proposal . proposalId }
321- isActive = { proposal . status === "Active" }
322- />
323- </ TabsContent >
324- ) }
325- { shouldShowPropdatesTab && (
326- < TabsContent value = "propdates" className = "mt-6" >
327- < Propdates
328- proposalId = { proposal . proposalId }
329- proposer = { proposal . proposer }
330- targets = { proposal . targets }
331- />
332- </ TabsContent >
333- ) }
334- </ Tabs >
335- ) : (
336- < div className = "space-y-6" >
302+ ) }
303+ < TabsContent
304+ value = "details"
305+ className = { `space-y-6 ${ shouldShowTabsList ? "mt-6" : "" } ` }
306+ >
337307 < ProposalDescriptionCard description = { proposal . description } />
338308 { hasTransactionData && (
339309 < Card >
@@ -346,12 +316,37 @@ export function ProposalDetail({ proposal }: ProposalDetailProps) {
346316 values = { proposal . values }
347317 signatures = { proposal . signatures }
348318 calldatas = { proposal . calldatas }
319+ descriptions = { ( proposal as MultiChainProposal ) . txDescriptions }
349320 />
350321 </ CardContent >
351322 </ Card >
352323 ) }
353- </ div >
354- ) }
324+ </ TabsContent >
325+ { shouldShowVotesTab && (
326+ < TabsContent value = "votes" className = "mt-6 space-y-6" >
327+ < ProposalVotesList
328+ votes = { votesList . map ( ( v ) => ( {
329+ voter : v . voter ,
330+ choice : v . choice ,
331+ votes : v . votes ,
332+ reason : ( v as { reason ?: string | null } ) . reason ?? null ,
333+ timestamp : ( v as { timestamp ?: number } ) . timestamp ,
334+ } ) ) }
335+ proposalId = { proposal . proposalId }
336+ isActive = { proposal . status === "Active" }
337+ />
338+ </ TabsContent >
339+ ) }
340+ { shouldShowPropdatesTab && (
341+ < TabsContent value = "propdates" className = "mt-6" >
342+ < Propdates
343+ proposalId = { proposal . proposalId }
344+ proposer = { proposal . proposer }
345+ targets = { proposal . targets }
346+ />
347+ </ TabsContent >
348+ ) }
349+ </ Tabs >
355350 </ div >
356351 ) ;
357352}
0 commit comments