Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stop using loader functions, part 1 #1306

Merged
merged 16 commits into from
Jun 19, 2024
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
6 changes: 6 additions & 0 deletions .changeset/beige-fans-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@penumbra-zone/zquery': patch
'minifront': patch
---

Stop using loader functions for most routes; fix typings issue in ZQuery
1 change: 1 addition & 0 deletions apps/minifront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-portal": "^1.0.4",
"@remix-run/router": "^1.16.1",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my comment in root-router

"@repo/ui": "workspace:*",
"@tanstack/react-query": "4.36.1",
"bech32": "^2.0.0",
Expand Down
6 changes: 5 additions & 1 deletion apps/minifront/src/abort-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ const retry = async (fn: () => boolean, ms = 500, rate = Math.max(ms / 10, 50))
* timeout. This is a temporary solution until loaders properly await Prax
* connection.
*/
export const abortLoader = async (): Promise<void> => {
export const abortLoader = async (): Promise<null> => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this the trick, ie. returning null, that enables instantaneous route changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, no — loaders are required to return something rather than void. The instantaneous route changes are due to the fact that we no longer have loaders that are awaiting any slow requests.

await throwIfPraxNotInstalled();
await retry(() => isPraxConnected());
throwIfPraxNotConnected();

// Loaders are required to return a value, even if it's null. By returning
// `null` here, we can use this loader directly in the router.
return null;
};
15 changes: 4 additions & 11 deletions apps/minifront/src/components/dashboard/assets-table/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { LoaderFunction, useLoaderData } from 'react-router-dom';
import { AddressIcon } from '@repo/ui/components/ui/address-icon';
import { AddressComponent } from '@repo/ui/components/ui/address-component';
import { BalancesByAccount, getBalancesByAccount } from '../../../fetchers/balances/by-account';
import {
Table,
TableBody,
Expand All @@ -11,20 +9,15 @@ import {
TableRow,
} from '@repo/ui/components/ui/table';
import { ValueViewComponent } from '@repo/ui/components/ui/tx/view/value';
import { abortLoader } from '../../../abort-loader';
import { EquivalentValues } from './equivalent-values';
import { Fragment } from 'react';
import { shouldDisplay } from './helpers';

export const AssetsLoader: LoaderFunction = async (): Promise<BalancesByAccount[]> => {
await abortLoader();
return await getBalancesByAccount();
};
import { useBalancesByAccount } from '../../../state/balances';
Comment on lines -19 to -22
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that I've deleted the loaders for a bunch of routes, but I've replaced their loaders in root-router.tsx with abortLoader. So, the abortLoader functionality is still there; it's just not wrapped in a separate loader colocated with the component.


export default function AssetsTable() {
const balancesByAccount = useLoaderData() as BalancesByAccount[];
const balancesByAccount = useBalancesByAccount();

Copy link
Contributor

@TalDerei TalDerei Jun 18, 2024

Choose a reason for hiding this comment

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

we were previously using abortLoader to sanity-check that certain preconditions were met before proceeding with a data-fetching operation. If any of the checks failed, an exception was thrown, which aborted the loading process. Where is the equivalent of this happening in zquery land?

Copy link
Contributor

Choose a reason for hiding this comment

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

it should be unnecessary - the loader check was only present because the loaders were suppressing the check done at page layout

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've actually moved abortLoader to root-router.tsx.

@turbocrime are you saying I can remove them, then?

if (balancesByAccount.length === 0) {
if (balancesByAccount.data?.length === 0) {
return (
<div className='flex flex-col gap-6'>
<p>
Expand All @@ -40,7 +33,7 @@ export default function AssetsTable() {

return (
<Table>
{balancesByAccount.map(account => (
{balancesByAccount.data?.map(account => (
<Fragment key={account.account}>
<TableHeader className='group'>
<TableRow>
Expand Down
13 changes: 4 additions & 9 deletions apps/minifront/src/components/dashboard/transaction-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import {
} from '@repo/ui/components/ui/table';
import { Link } from 'react-router-dom';
import { shorten } from '@penumbra-zone/types/string';
import { useStore } from '../../state';
import { memo, useEffect } from 'react';
import { TransactionSummary } from '../../state/transactions';
import { memo } from 'react';
import { TransactionSummary, useSummaries } from '../../state/transactions';

export default function TransactionTable() {
const { summaries, loadSummaries } = useStore(store => store.transactions);

useEffect(() => void loadSummaries(), [loadSummaries]);
const summaries = useSummaries();

return (
<Table className='md:table'>
Expand All @@ -28,9 +25,7 @@ export default function TransactionTable() {
</TableRow>
</TableHeader>
<TableBody>
{summaries.map(summary => (
<Row key={summary.hash} summary={summary} />
))}
{summaries.data?.map(summary => <Row key={summary.hash} summary={summary} />)}
</TableBody>
</Table>
);
Expand Down
2 changes: 1 addition & 1 deletion apps/minifront/src/components/ibc/ibc-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export const IbcLoader: LoaderFunction = async (): Promise<IbcLoaderResponse> =>
const initialSelection = filterBalancesPerChain(
assetBalances,
initialChain,
stakingTokenMetadata,
assets,
stakingTokenMetadata,
)[0];

// set initial account if accounts exist and asset if account has asset list
Expand Down
11 changes: 9 additions & 2 deletions apps/minifront/src/components/ibc/ibc-out/ibc-out-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ import InputToken from '../../shared/input-token';
import { InputBlock } from '../../shared/input-block';
import { IbcLoaderResponse } from '../ibc-loader';
import { LockOpen2Icon } from '@radix-ui/react-icons';
import { useStakingTokenMetadata } from '../../../state/shared';

export const IbcOutForm = () => {
const { balances, stakingTokenMetadata, assets } = useLoaderData() as IbcLoaderResponse;
const stakingTokenMetadata = useStakingTokenMetadata();
const { balances, assets } = useLoaderData() as IbcLoaderResponse;
const {
sendIbcWithdraw,
destinationChainAddress,
Expand All @@ -25,7 +27,12 @@ export const IbcOutForm = () => {
setSelection,
chain,
} = useStore(ibcOutSelector);
const filteredBalances = filterBalancesPerChain(balances, chain, stakingTokenMetadata, assets);
const filteredBalances = filterBalancesPerChain(
balances,
chain,
assets,
stakingTokenMetadata.data,
);
const validationErrors = useStore(ibcValidationErrors);

return (
Expand Down
20 changes: 11 additions & 9 deletions apps/minifront/src/components/root-router.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { createHashRouter, redirect } from 'react-router-dom';
import { PagePath } from './metadata/paths';
import { Layout } from './layout';
import AssetsTable, { AssetsLoader } from './dashboard/assets-table';
import AssetsTable from './dashboard/assets-table';
import TransactionTable from './dashboard/transaction-table';
import { DashboardLayout } from './dashboard/layout';
import { TxDetails, TxDetailsErrorBoundary, TxDetailsLoader } from './tx-details';
import { TxDetails, TxDetailsErrorBoundary } from './tx-details';
import { SendLayout } from './send/layout';
import { SendAssetBalanceLoader, SendForm } from './send/send-form';
import { SendForm } from './send/send-form';
import { Receive } from './send/receive';
import { ErrorBoundary } from './shared/error-boundary';
import { SwapLayout } from './swap/layout';
import { SwapLoader } from './swap/swap-loader';
import { StakingLayout, StakingLoader } from './staking/layout';
import { StakingLayout } from './staking/layout';
import { IbcLoader } from './ibc/ibc-loader';
import { IbcLayout } from './ibc/layout';
import { abortLoader } from '../abort-loader';
import type { Router } from '@remix-run/router';

export const rootRouter = createHashRouter([
export const rootRouter: Router = createHashRouter([
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding : Router fixes a longtime TypeScript error we were getting in this file that for some reason didn't break the build.

Copy link
Contributor

@TalDerei TalDerei Jun 18, 2024

Choose a reason for hiding this comment

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

can we add some comments somewhere on the high-level similarities and differences between react-router loaders and zquery?

From what I can tell, both support:

  • async data fetching
  • state access to access fetched data in components

In terms of their differences, this is where some more clarification would be helpful. Zquery anchors on the idea of global state management, where state can be updated and shared across an application, right? React-router on the other hand fetches data specifically for a single route, and rather than being stored globally, it's only accessible by a specific component. I guess I'm wondering where component-level data fetching, a staple of the latter case, would generally be more preferred?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, hm, I'm realizing I should write up an ADR for all of this.

The idea is that we want to consolidate our state into one state machine, rather than having it split among several (Zustand, react-router, React Query, etc.).

I would assume that component-level data fetching as provided by React Query is for simpler applications that don't use a global data store like Zustand. It's also useful for server-side applications that load data and render it before sending HTML down to the browser — unlike what we're doing with our entirely client-side rendering.

{
path: '/',
element: <Layout />,
Expand All @@ -28,7 +30,7 @@ export const rootRouter = createHashRouter([
children: [
{
index: true,
loader: AssetsLoader,
loader: abortLoader,
element: <AssetsTable />,
},
{
Expand All @@ -43,7 +45,7 @@ export const rootRouter = createHashRouter([
children: [
{
index: true,
loader: SendAssetBalanceLoader,
loader: abortLoader,
element: <SendForm />,
},
{
Expand All @@ -59,13 +61,13 @@ export const rootRouter = createHashRouter([
},
{
path: PagePath.TRANSACTION_DETAILS,
loader: TxDetailsLoader,
loader: abortLoader,
element: <TxDetails />,
errorElement: <TxDetailsErrorBoundary />,
},
{
path: PagePath.STAKING,
loader: StakingLoader,
loader: abortLoader,
element: <StakingLayout />,
},
{
Expand Down
46 changes: 15 additions & 31 deletions apps/minifront/src/components/send/send-form/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,23 @@
import { Button } from '@repo/ui/components/ui/button';
import { Input } from '@repo/ui/components/ui/input';
import { useStore } from '../../../state';
import { sendSelector, sendValidationErrors } from '../../../state/send';
import {
sendSelector,
sendValidationErrors,
useTransferableBalancesResponses,
} from '../../../state/send';
import { InputBlock } from '../../shared/input-block';
import { LoaderFunction, useLoaderData } from 'react-router-dom';
import { useMemo, useState } from 'react';
import { getTransferableBalancesResponses, penumbraAddrValidation } from '../helpers';
import { abortLoader } from '../../../abort-loader';
import { penumbraAddrValidation } from '../helpers';
import InputToken from '../../shared/input-token';
import { useRefreshFee } from './use-refresh-fee';
import { GasFee } from '../../shared/gas-fee';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { getStakingTokenMetadata } from '../../../fetchers/registry';
import { hasStakingToken } from '../../../fetchers/staking-token';

export interface SendLoaderResponse {
assetBalances: BalancesResponse[];
stakingAssetMetadata: Metadata;
}

export const SendAssetBalanceLoader: LoaderFunction = async (): Promise<SendLoaderResponse> => {
await abortLoader();
const assetBalances = await getTransferableBalancesResponses();

if (assetBalances[0]) {
// set initial account if accounts exist and asset if account has asset list
useStore.setState(state => {
state.send.selection = assetBalances[0];
});
}
const stakingAssetMetadata = await getStakingTokenMetadata();

return { assetBalances, stakingAssetMetadata };
};
import { useStakingTokenMetadata } from '../../../state/shared';

export const SendForm = () => {
const { assetBalances, stakingAssetMetadata } = useLoaderData() as SendLoaderResponse;
const stakingTokenMetadata = useStakingTokenMetadata();
const transferableBalancesResponses = useTransferableBalancesResponses();
const {
selection,
amount,
Expand All @@ -56,7 +37,10 @@ export const SendForm = () => {
const [showNonNativeFeeWarning, setshowNonNativeFeeWarning] = useState(false);

// Check if the user has native staking tokens
const stakingToken = hasStakingToken(assetBalances, stakingAssetMetadata);
const stakingToken = hasStakingToken(
transferableBalancesResponses.data,
stakingTokenMetadata.data,
);

useRefreshFee();

Expand Down Expand Up @@ -106,7 +90,7 @@ export const SendForm = () => {
checkFn: () => validationErrors.amountErr,
},
]}
balances={assetBalances}
balances={transferableBalancesResponses.data ?? []}
/>
{showNonNativeFeeWarning && (
<div className='rounded border border-yellow-500 bg-gray-800 p-4 text-yellow-500'>
Expand All @@ -121,7 +105,7 @@ export const SendForm = () => {
<GasFee
fee={fee}
feeTier={feeTier}
stakingAssetMetadata={stakingAssetMetadata}
stakingAssetMetadata={stakingTokenMetadata.data}
setFeeTier={setFeeTier}
/>

Expand Down
4 changes: 3 additions & 1 deletion apps/minifront/src/components/shared/gas-fee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ export const GasFee = ({
}: {
fee: Fee | undefined;
feeTier: FeeTier_Tier;
stakingAssetMetadata: Metadata;
stakingAssetMetadata?: Metadata;
setFeeTier: (feeTier: FeeTier_Tier) => void;
}) => {
if (!stakingAssetMetadata) return null;

let feeValueView: ValueView | undefined;
if (fee?.amount)
feeValueView = new ValueView({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { DelegationValueView } from '.';
import { render } from '@testing-library/react';
import {
Expand Down Expand Up @@ -47,7 +47,7 @@ const STAKING_TOKEN_METADATA = new Metadata({

const validatorInfo = new ValidatorInfo({
validator: {
identityKey: {},
identityKey: validatorIk,
fundingStreams: [
{
recipient: {
Expand Down Expand Up @@ -98,27 +98,32 @@ const valueView = new ValueView({
},
});

const mockUseStakingTokenMetadata = vi.hoisted(() => () => ({
data: STAKING_TOKEN_METADATA,
error: undefined,
loading: undefined,
}));

vi.mock('../../../../state/shared', async () => ({
...(await vi.importActual('../../../../state/shared')),
useStakingTokenMetadata: mockUseStakingTokenMetadata,
}));

describe('<DelegationValueView />', () => {
it('shows balance of the delegation token', () => {
const { container } = render(
<DelegationValueView valueView={valueView} stakingTokenMetadata={STAKING_TOKEN_METADATA} />,
);
const { container } = render(<DelegationValueView valueView={valueView} />);

expect(container).toHaveTextContent('1delUM(abc...xyz)');
});

it("shows the delegation token's equivalent value in terms of the staking token", () => {
const { container } = render(
<DelegationValueView valueView={valueView} stakingTokenMetadata={STAKING_TOKEN_METADATA} />,
);
const { container } = render(<DelegationValueView valueView={valueView} />);

expect(container).toHaveTextContent('1.33UM');
});

it('does not show other equivalent values', () => {
const { container } = render(
<DelegationValueView valueView={valueView} stakingTokenMetadata={STAKING_TOKEN_METADATA} />,
);
const { container } = render(<DelegationValueView valueView={valueView} />);

expect(container).not.toHaveTextContent('2.66SOT');
});
Expand Down
Loading
Loading