react-spectrum icon indicating copy to clipboard operation
react-spectrum copied to clipboard

Support client-side routing for all components with href prop

Open universse opened this issue 3 months ago โ€ข 7 comments

Provide a general summary of the issue here

From documentation, only Link and MenuItem are supported. Seems to be because only those 2 are making use of handleLinkClick. Other components, such as Tab, ListBoxItem etc, should work similarly.

๐Ÿค” Expected Behavior?

Using TanStack Router's createLink to wrap components with href prop allows client-side routing. Haven't got a chance to test with other routers.

๐Ÿ˜ฏ Current Behavior

Using TanStack Router's createLink to wrap components with href prop results in full page refresh.

๐Ÿ’ Possible Solution

No response

๐Ÿ”ฆ Context

No response

๐Ÿ–ฅ๏ธ Steps to Reproduce

https://react-spectrum.adobe.com/react-aria/routing.html#tanstack-router https://stackblitz.com/edit/tanstack-router-xkvvewjr?file=package.json,src%2Fmain.tsx&preset=node

Version

1.12.1

What browsers are you seeing the problem on?

Firefox

If other, please specify.

No response

What operating system are you using?

Windows 11

๐Ÿงข Your Company/Team

No response

๐Ÿ•ท Tracking Issue

No response

universse avatar Sep 24 '25 18:09 universse

https://github.com/TanStack/router/pull/4757#issuecomment-3118988281 has some additional information on why this is a bit tricky. Does something like https://stackblitz.com/edit/tanstack-router-qcvtkzgf?file=package.json,src%2Fmain.tsx&preset=node work for you in the meantime?

LFDanLu avatar Oct 08 '25 22:10 LFDanLu

thanks. that works for now.

universse avatar Oct 10 '25 13:10 universse

Didn't want to create a new issue since this one is still open but I thought I'd raise that it's not possible to use createLink from Tanstack Router on Table Row components since the method overrides the render props of children with its own props to indicate if a link isActive.

You can see it happening here https://github.com/TanStack/router/blob/b5ac27b9e764f33c6168ffdf8acb02cb91a95e23/packages/react-router/src/link.tsx#L576

So it's not possible to do something like this now


const RowLink = createLink(Row)

<Table aria-label="Fan lists">
    <TableHeader columns={columns}>{(column) => <Column {...column} />}</TableHeader>
    <TableBody
    >
        <Collection items={items}>
            {({ item }) => (
                <RowLink
                    columns={columns}
                    to={appWorkspaceSubscribersFanListIndexRoute.to}
                    params={{ fanListId: Number(item.id) }}
                >
                    {(column) => {  // <------ THESE TYPES ARE NOW TS ROUTER INSTEAD OF COLUMN TYPES
                        switch (column.id) {
                            case "name":
                                return <Cell typography={"h5"}>{item.name}</Cell>
                            case "actions":
                                return (
                                    <Cell className={TABLE_ACTIONS_CELL_CLASSNAMES}>
                                        <ActionsCell id={Number(item.id)} />
                                    </Cell>
                                )
                            default:
                                return <></>
                        }
                    }}
                </RowLink>
            )}
        </Collection>
    </TableBody>
</Table>

I (think) this is something that would have to be handled on RAC side...or maybe the request should be to allow passing existing render props on the TS Router side...I'm unsure


EDIT: I need to head to bed but the TS Router docs state you can create a wrapper to pass to createLink so my idea is to pass the column rendering as a prop instead of children, I am halfway there I think but not sure if there's a better approach...

interface MyRowProps<T extends object> extends RowProps<T> {
    renderColumn: (column: T) => React.ReactNode
}

function RACRowLink({ children, renderColumn, ...props }: MyRowProps<ColumnProps>) {
    return (
        <Row {...props}>
            {composeRenderProps(children, (children, props) => {
                return <>{renderColumn(props)}</>
            })}
        </Row>
    )
}

const RowLink = createLink(RACRowLink)

uncvrd avatar Oct 27 '25 07:10 uncvrd

I am also noticing full page reloads when using ListBoxItem and createLink

mattkinnersley avatar Oct 31 '25 16:10 mattkinnersley

@mattkinnersley check my comment for a workaround above in https://github.com/TanStack/router/pull/4757

uncvrd avatar Nov 09 '25 09:11 uncvrd

also here is my completed workaround for getting RAC Row to work nice with TS Router createLink. Really not a fan of creating a render prop just for this, so if anyone has a better idea please let me know

// Except<T> is from type-fest npm package to remove a property
interface RowLinkProps<T extends object> extends Except<RowProps<T>, "children"> {
    renderColumn: (column: T) => React.ReactElement
}

function RACRowLink({ renderColumn, ...props }: RowLinkProps<ColumnProps>) {
    return <Row {...props}>{renderColumn}</Row>
}

const RowLink = createLink(RACRowLink)

you can then use like this

<RowLink
    to={appWorkspaceSubscribersFanListIndexRoute.to}
    params={{ fanListId: Number(item.id) }}
    columns={columns}
    renderColumn={(column) => {
        switch (column.id) {
            case "name":
                return <Cell typography={"h5"}>{item.name}</Cell>
            case "actions":
                return (
                    <Cell className={TABLE_ACTIONS_CELL_CLASSNAMES}>
                        <ActionsCell id={Number(item.id)} />
                    </Cell>
                )
            default:
                return <></>
        }
    }}
/>

Thanks for any help!

uncvrd avatar Nov 09 '25 09:11 uncvrd

Just realized you can use <Collection> for the columns too but I think my original comment here is still worth taking a look at...anyways here's another workaround/solution for now

const RowLink = createLink(Row)

<Table aria-label="Fan lists">
    <TableHeader columns={columns}>{(column) => <Column {...column} />}</TableHeader>
    <TableBody
        renderEmptyState={() => (
            <TableEmptyState
                title="No Fan Lists Found"
                description="Fan Lists are used for Presave and Messaging Campaigns"
            />
        )}
    >
        <Collection items={items}>
            {({ item }) => (
                <RowLink
                    to={"/app/workspace/subscribers/fan-list/$fanListId"}
                    params={{ fanListId: Number(item.id) }}
                >
                    <Collection items={columns}>
                        {(column) => {
                            switch (column.id) {
                                case "name":
                                    return <Cell typography={"h5"}>{item.name}</Cell>
                                case "actions":
                                    return (
                                        <Cell className={TABLE_ACTIONS_CELL_CLASSNAMES}>
                                            <ActionsCell id={Number(item.id)} />
                                        </Cell>
                                    )
                                default:
                                    return <></>
                            }
                        }}
                    </Collection>
                </RowLink>
            )}
        </Collection>
        <InfoPageInfiniteSearchTableLoadMore />
    </TableBody>
</Table>

uncvrd avatar Nov 17 '25 08:11 uncvrd