Skip to content

Commit 059617b

Browse files
committed
feat: Reduce SSR hydration errors
1 parent 9b384ce commit 059617b

File tree

8 files changed

+50
-16
lines changed

8 files changed

+50
-16
lines changed

.changeset/proud-rabbits-read.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@aragon/gov-ui-kit': minor
3+
---
4+
5+
Fix ssr hydration errors

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const config = {
55
testEnvironment: 'jsdom',
66
collectCoverageFrom: ['./src/**/*.{ts,tsx}'],
7+
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
78
coveragePathIgnorePatterns: ['.d.ts', '.api.ts', 'index.ts', '.stories.tsx', './src/core/test/*'],
89
setupFilesAfterEnv: ['<rootDir>/src/core/test/setup.ts'],
910
globalSetup: '<rootDir>/src/core/test/globalSetup.ts',

src/modules/components/member/memberDataListItem/memberDataListItemStructure/memberDataListItemStructure.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ describe('<MemberDataListItem /> component', () => {
6767
expect(screen.getByRole('heading', { level: 3, name: '420.69K ETH Voting Power' })).toBeInTheDocument();
6868
});
6969

70-
it('renders a you tag when the user is the current connected account', () => {
70+
it('renders a you tag when the user is the current connected account', async () => {
7171
const address = '0x50ce432B38eE98dE5Fa375D5125aA6d0d054E662';
7272
useAccountMock.mockReturnValue({ isConnected: true, address } as unknown as wagmi.UseAccountReturnType);
7373
render(createTestComponent({ address }));
74-
expect(screen.getByText('You')).toBeInTheDocument();
74+
expect(await screen.findByText('You')).toBeInTheDocument();
7575
});
7676

7777
it('hides the voting power label when hideLabelTokenVoting is true', () => {

src/modules/components/member/memberDataListItem/memberDataListItemStructure/memberDataListItemStructure.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import classNames from 'classnames';
2+
import { useEffect, useState } from 'react';
23
import { useAccount } from 'wagmi';
34
import { DataList, Heading, NumberFormat, Tag, formatterUtils, type IDataListItemProps } from '../../../../../core';
45
import { addressUtils } from '../../../../utils';
@@ -58,7 +59,13 @@ export const MemberDataListItemStructure: React.FC<IMemberDataListItemProps> = (
5859

5960
const { copy } = useGukModulesContext();
6061

61-
const isCurrentUser = isConnected && address && addressUtils.isAddressEqual(currentUserAddress, address);
62+
// Avoid SSR/CSR hydration mismatches: the connected address is only known on
63+
// the client, so only show the "You" tag after mount.
64+
const [hasMounted, setHasMounted] = useState(false);
65+
useEffect(() => setHasMounted(true), []);
66+
67+
const isCurrentUser =
68+
hasMounted && isConnected && address && addressUtils.isAddressEqual(currentUserAddress, address);
6269

6370
const resolvedUserHandle = ensName != null && ensName.length > 0 ? ensName : addressUtils.truncateAddress(address);
6471

src/modules/components/proposal/proposalDataListItem/proposalDataListItemStructure/proposalDataListItemStructure.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('<ProposalDataListItemStructure/> component', () => {
4444
expect(screen.getByText(children)).toBeInTheDocument();
4545
});
4646

47-
it("renders 'You' as the publisher if the connected address is the publisher address", () => {
47+
it("renders 'You' as the publisher if the connected address is the publisher address", async () => {
4848
const publisher = { address: '0x0000000000000000000000000000000000000000', link: '#' };
4949

5050
useAccountMock.mockReturnValue({
@@ -54,7 +54,7 @@ describe('<ProposalDataListItemStructure/> component', () => {
5454

5555
render(createTestComponent({ publisher }));
5656

57-
expect(screen.getByRole('link', { name: 'You' })).toBeInTheDocument();
57+
expect(await screen.findByRole('link', { name: 'You' })).toBeInTheDocument();
5858
});
5959

6060
it('renders multiple publishers', () => {

src/modules/components/proposal/proposalDataListItem/proposalDataListItemStructure/proposalDataListItemStructure.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import classNames from 'classnames';
2+
import { useEffect, useMemo, useState } from 'react';
23
import { useAccount } from 'wagmi';
34
import { DataList, Heading, Link, Tag } from '../../../../../core';
45
import { addressUtils } from '../../../../utils/addressUtils';
@@ -8,13 +9,19 @@ import { type IProposalDataListItemStructureProps, type IPublisher } from './pro
89

910
export const maxPublishersDisplayed = 3;
1011

11-
const parsePublisher = (publisher: IPublisher, isConnected: boolean, connectedAddress: string | undefined) => {
12-
const publisherIsConnected = isConnected && addressUtils.isAddressEqual(publisher.address, connectedAddress);
12+
const parsePublisher = (
13+
publisher: IPublisher,
14+
canShowConnectedLabel: boolean,
15+
isConnected: boolean,
16+
connectedAddress: string | undefined,
17+
) => {
18+
const publisherIsConnected =
19+
canShowConnectedLabel && isConnected && addressUtils.isAddressEqual(publisher.address, connectedAddress);
1320
const publisherLabel = publisherIsConnected
1421
? 'You'
1522
: (publisher.name ?? addressUtils.truncateAddress(publisher.address));
1623

17-
return { label: publisherLabel, link: publisher.link };
24+
return { label: publisherLabel, link: publisher.link, address: publisher.address };
1825
};
1926

2027
export const ProposalDataListItemStructure: React.FC<IProposalDataListItemStructureProps> = (props) => {
@@ -37,9 +44,16 @@ export const ProposalDataListItemStructure: React.FC<IProposalDataListItemStruct
3744
const { address: connectedAddress, isConnected } = useAccount({ config });
3845
const { copy } = useGukModulesContext();
3946

40-
const parsedPublisher = Array.isArray(publisher)
41-
? publisher.map((p) => parsePublisher(p, isConnected, connectedAddress))
42-
: [parsePublisher(publisher, isConnected, connectedAddress)];
47+
// Avoid SSR/CSR hydration mismatches: the connected address is only known on
48+
// the client, so we only render the "You" label after mount.
49+
const [hasMounted, setHasMounted] = useState(false);
50+
useEffect(() => setHasMounted(true), []);
51+
52+
const parsedPublisher = useMemo(() => {
53+
return Array.isArray(publisher)
54+
? publisher.map((p) => parsePublisher(p, hasMounted, isConnected, connectedAddress))
55+
: [parsePublisher(publisher, hasMounted, isConnected, connectedAddress)];
56+
}, [publisher, hasMounted, isConnected, connectedAddress]);
4357

4458
const showParsedPublisher = parsedPublisher.length <= maxPublishersDisplayed;
4559

@@ -71,8 +85,8 @@ export const ProposalDataListItemStructure: React.FC<IProposalDataListItemStruct
7185
</span>
7286
)}
7387
{showParsedPublisher &&
74-
parsedPublisher.map(({ label, link }, index) => (
75-
<span key={label} className="truncate">
88+
parsedPublisher.map(({ address, label, link }, index) => (
89+
<span key={`${address}-${index.toString()}`} className="truncate">
7690
<object type="unknown" className="flex shrink">
7791
{link != null && (
7892
// Using solution from https://kizu.dev/nested-links/ to nest anchor tags

src/modules/components/vote/voteDataListItem/voteDataListItemStructure/voteDataListItemStructure.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ describe('<VoteDataListItemStructure /> component', () => {
7272
expect(screen.getByText(voter.name)).toBeInTheDocument();
7373
});
7474

75-
it('renders the "You" tag if the voter is the current user', () => {
75+
it('renders the "You" tag if the voter is the current user', async () => {
7676
const voter = { address: '0x1234567890123456789012345678901234567890' };
7777
useAccountSpy.mockReturnValue({ address: voter.address, isConnected: true } as wagmi.UseAccountReturnType);
7878

7979
render(createTestComponent({ voter }));
8080

81-
expect(screen.getByText('You')).toBeInTheDocument();
81+
expect(await screen.findByText('You')).toBeInTheDocument();
8282
});
8383

8484
it('renders "Your delegate" tag if the voter is a delegate of the current user', () => {

src/modules/components/vote/voteDataListItem/voteDataListItemStructure/voteDataListItemStructure.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import classNames from 'classnames';
2+
import { useEffect, useState } from 'react';
23
import { useAccount } from 'wagmi';
34
import { DataList, NumberFormat, Tag, formatterUtils, type IDataListItemProps } from '../../../../../core';
45
import { type ICompositeAddress } from '../../../../types';
@@ -55,7 +56,13 @@ export const VoteDataListItemStructure: React.FC<IVoteDataListItemStructureProps
5556

5657
const { copy } = useGukModulesContext();
5758

58-
const isCurrentUser = isConnected && addressUtils.isAddressEqual(currentUserAddress, voter.address);
59+
// Avoid SSR/CSR hydration mismatches: the connected address is only known on
60+
// the client, so only show the "You" tag after mount.
61+
const [hasMounted, setHasMounted] = useState(false);
62+
useEffect(() => setHasMounted(true), []);
63+
64+
const isCurrentUser =
65+
hasMounted && isConnected && addressUtils.isAddressEqual(currentUserAddress, voter.address);
5966

6067
const resolvedUserHandle =
6168
voter.name != null && voter.name.length > 0 ? voter.name : addressUtils.truncateAddress(voter.address);

0 commit comments

Comments
 (0)