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
5 changes: 5 additions & 0 deletions .changeset/proud-rabbits-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aragon/gov-ui-kit': minor
---

Fix ssr hydration errors
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const config = {
testEnvironment: 'jsdom',
collectCoverageFrom: ['./src/**/*.{ts,tsx}'],
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
coveragePathIgnorePatterns: ['.d.ts', '.api.ts', 'index.ts', '.stories.tsx', './src/core/test/*'],
setupFilesAfterEnv: ['<rootDir>/src/core/test/setup.ts'],
globalSetup: '<rootDir>/src/core/test/globalSetup.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ describe('<MemberDataListItem /> component', () => {
expect(screen.getByRole('heading', { level: 3, name: '420.69K ETH Voting Power' })).toBeInTheDocument();
});

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

it('hides the voting power label when hideLabelTokenVoting is true', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { DataList, Heading, NumberFormat, Tag, formatterUtils, type IDataListItemProps } from '../../../../../core';
import { addressUtils } from '../../../../utils';
Expand Down Expand Up @@ -58,7 +59,13 @@ export const MemberDataListItemStructure: React.FC<IMemberDataListItemProps> = (

const { copy } = useGukModulesContext();

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

const isCurrentUser =
hasMounted && isConnected && address && addressUtils.isAddressEqual(currentUserAddress, address);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('<ProposalDataListItemStructure/> component', () => {
expect(screen.getByText(children)).toBeInTheDocument();
});

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

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

render(createTestComponent({ publisher }));

expect(screen.getByRole('link', { name: 'You' })).toBeInTheDocument();
expect(await screen.findByRole('link', { name: 'You' })).toBeInTheDocument();
});

it('renders multiple publishers', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { useAccount } from 'wagmi';
import { DataList, Heading, Link, Tag } from '../../../../../core';
import { addressUtils } from '../../../../utils/addressUtils';
Expand All @@ -8,13 +9,19 @@ import { type IProposalDataListItemStructureProps, type IPublisher } from './pro

export const maxPublishersDisplayed = 3;

const parsePublisher = (publisher: IPublisher, isConnected: boolean, connectedAddress: string | undefined) => {
const publisherIsConnected = isConnected && addressUtils.isAddressEqual(publisher.address, connectedAddress);
const parsePublisher = (
publisher: IPublisher,
canShowConnectedLabel: boolean,
isConnected: boolean,
connectedAddress: string | undefined,
) => {
const publisherIsConnected =
canShowConnectedLabel && isConnected && addressUtils.isAddressEqual(publisher.address, connectedAddress);
const publisherLabel = publisherIsConnected
? 'You'
: (publisher.name ?? addressUtils.truncateAddress(publisher.address));

return { label: publisherLabel, link: publisher.link };
return { label: publisherLabel, link: publisher.link, address: publisher.address };
};

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

const parsedPublisher = Array.isArray(publisher)
? publisher.map((p) => parsePublisher(p, isConnected, connectedAddress))
: [parsePublisher(publisher, isConnected, connectedAddress)];
// Avoid SSR/CSR hydration mismatches: the connected address is only known on
// the client, so we only render the "You" label after mount.
const [hasMounted, setHasMounted] = useState(false);
useEffect(() => setHasMounted(true), []);

const parsedPublisher = useMemo(() => {
return Array.isArray(publisher)
? publisher.map((p) => parsePublisher(p, hasMounted, isConnected, connectedAddress))
: [parsePublisher(publisher, hasMounted, isConnected, connectedAddress)];
}, [publisher, hasMounted, isConnected, connectedAddress]);

const showParsedPublisher = parsedPublisher.length <= maxPublishersDisplayed;

Expand Down Expand Up @@ -71,8 +85,8 @@ export const ProposalDataListItemStructure: React.FC<IProposalDataListItemStruct
</span>
)}
{showParsedPublisher &&
parsedPublisher.map(({ label, link }, index) => (
<span key={label} className="truncate">
parsedPublisher.map(({ address, label, link }, index) => (
<span key={`${address}-${index.toString()}`} className="truncate">
<object type="unknown" className="flex shrink">
{link != null && (
// Using solution from https://kizu.dev/nested-links/ to nest anchor tags
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,13 @@ describe('<VoteDataListItemStructure /> component', () => {
expect(screen.getByText(voter.name)).toBeInTheDocument();
});

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

render(createTestComponent({ voter }));

expect(screen.getByText('You')).toBeInTheDocument();
expect(await screen.findByText('You')).toBeInTheDocument();
});

it('renders "Your delegate" tag if the voter is a delegate of the current user', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { DataList, NumberFormat, Tag, formatterUtils, type IDataListItemProps } from '../../../../../core';
import { type ICompositeAddress } from '../../../../types';
Expand Down Expand Up @@ -55,7 +56,12 @@ export const VoteDataListItemStructure: React.FC<IVoteDataListItemStructureProps

const { copy } = useGukModulesContext();

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

const isCurrentUser = hasMounted && isConnected && addressUtils.isAddressEqual(currentUserAddress, voter.address);

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