Skip to content

Commit

Permalink
Reduce INP for ChainSelect dropdown(#233)
Browse files Browse the repository at this point in the history
- Reduce INP for ChainSelect and ChainSelectHref
- Create ThemeScript to prevent flash of light on load

Co-authored-by: Rosco Kalis <[email protected]>
  • Loading branch information
Dozie2001 and rkalis authored Oct 31, 2024
1 parent 3c4a3a9 commit 89e47e9
Show file tree
Hide file tree
Showing 12 changed files with 60 additions and 38 deletions.
16 changes: 16 additions & 0 deletions app/ThemeScript.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// We inject the theme script to prevent flash of white when the page loads
const ThemeScript = () => {
const themeScript = `
(function() {
const storedTheme = localStorage.getItem('theme')
if (storedTheme === '"light"') return;
if (storedTheme === '"dark"' || window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark')
}
})()
`;

return <script dangerouslySetInnerHTML={{ __html: themeScript }} />;
};

export default ThemeScript;
7 changes: 4 additions & 3 deletions app/[locale]/exploits/[slug]/ExploitChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import AddressForm from 'components/exploits/AddressForm';
import ExploitChecker from 'components/exploits/ExploitChecker';
import { AddressPageContextProvider } from 'lib/hooks/page-context/AddressPageContext';
import { Exploit, getUniqueChainIds } from 'lib/utils/exploits';
import { Suspense, useState } from 'react';
import { Suspense, useMemo, useState } from 'react';
import { Address } from 'viem';

interface Props {
Expand All @@ -13,11 +13,12 @@ interface Props {

const ExploitCheckerWrapper = ({ exploit }: Props) => {
const [address, setAddress] = useState<Address | undefined>();
const chainIds = useMemo(() => getUniqueChainIds(exploit), [exploit]);

return (
<Suspense>
<AddressPageContextProvider address={address} initialChainId={getUniqueChainIds(exploit)[0]}>
<AddressForm onSubmit={setAddress} chainIds={getUniqueChainIds(exploit)} />
<AddressPageContextProvider address={address} initialChainId={chainIds[0]}>
<AddressForm onSubmit={setAddress} chainIds={chainIds} />
<ExploitChecker exploit={exploit} />
</AddressPageContextProvider>
</Suspense>
Expand Down
2 changes: 2 additions & 0 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { SpeedInsights } from '@vercel/speed-insights/next';
import Analytics from 'app/Analytics';
import ThemeScript from 'app/ThemeScript';
import ToastifyConfig from 'components/common/ToastifyConfig';
import TopLoader from 'components/common/TopLoader';
import Footer from 'components/footer/Footer';
Expand Down Expand Up @@ -62,6 +63,7 @@ const MainLayout = ({ children, params }: Props) => {
return (
<html lang={params.locale}>
<head>
<ThemeScript />
<Analytics />
</head>
<body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@

import ChainSelectHref from 'components/common/select/ChainSelectHref';
import { getChainSlug } from 'lib/utils/chains';
import { useCallback } from 'react';

interface Props {
chainId: number;
}

// This is a wrapper around ChainSelectHref because we cannot pass the getUrl function as a prop from a server component
const AddNetworkChainSelect = ({ chainId }: Props) => {
return (
<ChainSelectHref
instanceId="add-network-chain-select"
selected={chainId}
getUrl={(chainId) => `/learn/wallets/add-network/${getChainSlug(chainId)}`}
showNames
/>
);
const getUrl = useCallback((chainId) => `/learn/wallets/add-network/${getChainSlug(chainId)}`, []);
return <ChainSelectHref instanceId="add-network-chain-select" selected={chainId} getUrl={getUrl} showNames />;
};

export default AddNetworkChainSelect;
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@

import ChainSelectHref from 'components/common/select/ChainSelectHref';
import { getChainSlug } from 'lib/utils/chains';
import { useCallback } from 'react';

interface Props {
chainId: number;
}

// This is a wrapper around ChainSelectHref because we cannot pass the getUrl function as a prop from a server component
const TokenApprovalCheckerChainSelect = ({ chainId }: Props) => {
return (
<ChainSelectHref
instanceId="tac-chain-select"
selected={chainId}
getUrl={(chainId) => `/token-approval-checker/${getChainSlug(chainId)}`}
showNames
/>
);
const getUrl = useCallback((chainId) => `/token-approval-checker/${getChainSlug(chainId)}`, []);
return <ChainSelectHref instanceId="tac-chain-select" selected={chainId} getUrl={getUrl} showNames />;
};

export default TokenApprovalCheckerChainSelect;
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const BatchRevokeModalWithButton = ({ table }: Props) => {
</div>
<div className="h-[46vh] w-full overflow-scroll whitespace-nowrap scrollbar-hide">
<table className="w-full border-collapse">
<thead className="sticky top-0 bg-white z-50">
<thead className="sticky top-0 bg-white dark:bg-black z-50">
<tr>
<th className="py-2">#</th>
<th>{t('address.headers.asset')}</th>
Expand Down
3 changes: 2 additions & 1 deletion components/common/ChainLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useMounted } from 'lib/hooks/useMounted';
import { getChainLogo, getChainName, isSupportedChain } from 'lib/utils/chains';
import { memo } from 'react';
import { twMerge } from 'tailwind-merge';
import Logo from './Logo';
import PlaceholderIcon from './PlaceholderIcon';
Expand Down Expand Up @@ -38,4 +39,4 @@ const ChainLogo = ({ chainId, size, tooltip, className, checkMounted }: Props) =
return <Logo src={src} alt={name} size={size} border className={classes} />;
};

export default ChainLogo;
export default memo(ChainLogo);
5 changes: 4 additions & 1 deletion components/common/select/ChainSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ChainLogo from 'components/common/ChainLogo';
import { useColorTheme } from 'lib/hooks/useColorTheme';
import { CHAIN_SELECT_MAINNETS, CHAIN_SELECT_TESTNETS, getChainName, isSupportedChain } from 'lib/utils/chains';
import { useTranslations } from 'next-intl';
import { memo } from 'react';
import PlaceholderIcon from '../PlaceholderIcon';
import SearchableSelect from './SearchableSelect';

Expand Down Expand Up @@ -77,8 +78,10 @@ const ChainSelect = ({ onSelect, selected, menuAlign, chainIds, instanceId, show
minMenuWidth="14.5rem"
placeholder={<PlaceholderIcon size={24} border />}
menuAlign={menuAlign}
// Note: when searching, option do get unmounted, so there's still some optimization to be done here
keepMounted
/>
);
};

export default ChainSelect;
export default memo(ChainSelect);
3 changes: 2 additions & 1 deletion components/common/select/ChainSelectHref.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ChainLogo from 'components/common/ChainLogo';
import { useRouter } from 'lib/i18n/navigation';
import { CHAIN_SELECT_MAINNETS, CHAIN_SELECT_TESTNETS, getChainName, isSupportedChain } from 'lib/utils/chains';
import { useTranslations } from 'next-intl';
import { memo } from 'react';
import Button from '../Button';
import PlaceholderIcon from '../PlaceholderIcon';
import SearchableSelect from './SearchableSelect';
Expand Down Expand Up @@ -89,4 +90,4 @@ const ChainSelectHref = ({ selected, chainIds, getUrl, instanceId, menuAlign, sh
);
};

export default ChainSelectHref;
export default memo(ChainSelectHref);
22 changes: 14 additions & 8 deletions components/common/select/SearchableSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
'use client';

import { ReactNode, Ref, forwardRef, useEffect, useRef, useState } from 'react';
import { ActionMeta, FormatOptionLabelMeta, GroupBase, OnChangeValue, SelectInstance } from 'react-select';
import { forwardRef, ReactNode, Ref, useEffect, useRef, useState } from 'react';
import {
ActionMeta,
createFilter,
FormatOptionLabelMeta,
GroupBase,
OnChangeValue,
SelectInstance,
} from 'react-select';

import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';

import Select, { Props as SelectProps } from 'components/common/select/Select';
import Button from '../Button';
import Chevron from '../Chevron';

import { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
import type { FilterOptionOption } from 'react-select/dist/declarations/src/filters';
import { twMerge } from 'tailwind-merge';

interface Props<O, I extends boolean, G extends GroupBase<O>> extends SelectProps<O, I, G> {
Expand Down Expand Up @@ -42,10 +49,9 @@ const SearchableSelect = <O, I extends boolean, G extends GroupBase<O>>(props: P
handleSelectClose();
};

const handleFiltering = (option: FilterOptionOption<O>, inputValue: string) => {
const lowerCaseValue = inputValue.toLowerCase();
return option.value.toLowerCase().includes(lowerCaseValue);
};
const filterOption = createFilter({
stringify: (option: FilterOptionOption<O>) => option.value,
});

const formatOptionLabel = (option: O, formatOptionLabelMeta: FormatOptionLabelMeta<O>) => {
// 'value' context is handled separately in TargetButton
Expand All @@ -68,7 +74,7 @@ const SearchableSelect = <O, I extends boolean, G extends GroupBase<O>>(props: P
onChange={onChange}
className="shrink-0"
menuIsOpen={props.keepMounted ? true : isSelectOpen}
filterOption={handleFiltering}
filterOption={filterOption}
minControlWidth={props.minMenuWidth}
formatOptionLabel={props.formatOptionLabel ? formatOptionLabel : undefined}
components={{ DropdownIndicator: CustomDropdownIndicator, ...props.components }}
Expand Down
7 changes: 6 additions & 1 deletion components/common/select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ const Select = <O, I extends boolean, G extends GroupBase<O>>(props: Props<O, I,
{...props}
ref={props.selectRef}
className={twMerge(props.className)}
components={{ IndicatorSeparator: null, ClearIndicator: () => null, Option, ...props.components }}
components={{
IndicatorSeparator: null,
ClearIndicator: () => null,
Option,
...props.components,
}}
classNames={{
control: (state) =>
twMerge(
Expand Down
9 changes: 3 additions & 6 deletions components/header/WalletIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useMounted } from 'lib/hooks/useMounted';
import { useCallback } from 'react';
import { useAccount, useSwitchChain } from 'wagmi';
import ChainSelect from '../common/select/ChainSelect';
import WalletIndicatorDropdown from './WalletIndicatorDropdown';
Expand All @@ -17,18 +18,14 @@ const WalletIndicator = ({ menuAlign, size, style, className }: Props) => {
const isMounted = useMounted();
const { address: account, chain } = useAccount();
const { switchChain } = useSwitchChain();
const onSelect = useCallback((chainId: number) => switchChain({ chainId }), [switchChain]);

if (!isMounted) return null;

return (
<div className="flex gap-2">
{account && (
<ChainSelect
instanceId="global-chain-select"
onSelect={(chainId) => switchChain({ chainId })}
selected={chain?.id}
menuAlign={menuAlign}
/>
<ChainSelect instanceId="global-chain-select" onSelect={onSelect} selected={chain?.id} menuAlign={menuAlign} />
)}
<WalletIndicatorDropdown size={size} style={style} className={className} />
</div>
Expand Down

0 comments on commit 89e47e9

Please sign in to comment.