router icon indicating copy to clipboard operation
router copied to clipboard

ToOptions doesnt't satisfy <Link> props.

Open leqwasd opened this issue 1 year ago • 28 comments

Describe the bug

  1. thing I need - a Component, that wraps <Link>, so I can add predefined styles to all of my links. To use "more advanced" paths with them, I found a Type - ToOptions (https://tanstack.com/router/v1/docs/framework/react/api/router/ToOptionsType), that I can pass to Link. So I created my own Link component that in its basic form looks like this:
import { Link, ToOptions } from '@tanstack/react-router';
import * as React from 'react';
type MyLinkProps = {
  toOptions: ToOptions;
};
const MyLink: React.FC<React.PropsWithChildren<MyLinkProps>> = ({
  children,
  toOptions,
}) => {
  return <Link {...toOptions}>{children}</Link>;
};
export default MyLink;

It works - I get a nice help from TypeScript autocompletes. It says - that I need "from" property in some situations.

  1. I want routes that looks like this: posts/$id posts/$id/A posts/$id/B

In posts/$id.tsx - I display <Outlet /> so I can see the subroutes.

If you run attached project like this - it works, MyLink component is fine. (See here) https://stackblitz.com/edit/github-rb4ewj?file=src%2FMyLink.tsx

When I navigate to posts/$id - <Outlet /> contains nothing... So - I was wondering - and added a new file posts/$id/index.tsx. (I created a Fork from the previous stackblitz here: https://stackblitz.com/edit/github-rb4ewj-tg9tx2?file=src%2FMyLink.tsx) And this is where things go wrong - Link now complains about something not satisfying something...

Type '{ children: ReactNode; to: "/" | "/posts/$id" | "/posts/$id/A" | "/posts/$id/B" | "/posts/$id/"; hash?: true | Updater<string> | undefined; state?: true | NonNullableUpdater<HistoryState> | undefined; from?: RoutePathsAutoComplete<...> | undefined; search?: true | ... 1 more ... | undefined; params?: true | ... 1 mo...' is not assignable to type 'IntrinsicAttributes & ({ to?: ToPathOption<Route<any, "/", "/", string, "__root__", RootSearchSchema, RootSearchSchema, RootSearchSchema, ... 12 more ..., any>, string, "/" | ... 3 more ... | "/posts/$id/"> | undefined; hash?: true | ... 1 more ... | undefined; state?: true | ... 1 more ... | undefined; from?: Route...'.
  Type '{ children: ReactNode; to: "/" | "/posts/$id" | "/posts/$id/A" | "/posts/$id/B" | "/posts/$id/"; hash?: true | Updater<string> | undefined; state?: true | NonNullableUpdater<HistoryState> | undefined; from?: RoutePathsAutoComplete<...> | undefined; search?: true | ... 1 more ... | undefined; params?: true | ... 1 mo...' is not assignable to type 'MakePathParamOptions<true | ParamsReducer<{} | {} | { id: string; } | ({ id: string; } & {}), {} | {} | ({ id: string; } & {})>>'.
    Types of property 'params' are incompatible.
      Type 'true | ((current: {} | {} | { id: string; } | ({ id: string; } & {})) => never) | undefined' is not assignable to type 'true | ParamsReducer<{} | {} | { id: string; } | ({ id: string; } & {}), {} | {} | ({ id: string; } & {})>'.
        Type 'undefined' is not assignable to type 'true | ParamsReducer<{} | {} | { id: string; } | ({ id: string; } & {}), {} | {} | ({ id: string; } & {})>'.(2322)

All I did - was just create an index.tsx under posts/$id folder.

maybe I am just doing this wrong? How else I can make a default subroute? I do need a common component that wraps children (in this case, it is /src/routes/posts/$id.tsx

Your Example Website or App

https://stackblitz.com/edit/github-rb4ewj?file=src%2FMyLink.tsx

Steps to Reproduce the Bug or Issue

  1. Go here: https://stackblitz.com/edit/github-rb4ewj?file=src%2FMyLink.tsx
  2. Add index.tsx file under /src/routes/posts/$id/ folder (you might neeed to restart vite dev so it regenreates the content for the file)
  3. Open MyLink component and see the issue.

Expected behavior

I expect Link to not complain about ToOptions passed to it.

Screenshots or Videos

No response

Platform

  • OS: ?
  • Browser: ? "@tanstack/react-router": "^1.19.0",

Additional context

No response

leqwasd avatar Mar 07 '24 11:03 leqwasd

ToOptions wouldn't satisfy all the possible options that a <Link> could take.

Try this.

import { LinkProps, Registered router } from "@tanstack/react-router"

interface MyLinkProps {
  linkProps: LinkProps<RegisteredRouter['routeTree']>
}

...
<Link params {...props.linkProps}>

SeanCassiere avatar Mar 07 '24 14:03 SeanCassiere

  1. LinkProps includes RegisteredRouter by default
export type LinkProps<TRouteTree extends AnyRoute = RegisteredRouter['routeTree'], TFrom ....

And it doesn't work either.

Type '{ children: ((string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | ((state: { ...; }) => ReactNode)) & (string | ... 4 more ... | ReactPortal)) | null | undefined; ... 287 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'IntrinsicAttributes & ({ to?: ToPathOption<Route<any, "/", "/", string, "__root__", RootSearchSchema, RootSearchSchema, RootSearchSchema, ... 12 more ..., any>, string, "/" | ... 3 more ... | "/posts/$id/"> | undefined; hash?: true | ... 1 more ... | undefined; state?: true | ... 1 more ... | undefined; from?: Route...'.
  Type '{ children: ((string | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | ((state: { ...; }) => ReactNode)) & (string | ... 4 more ... | ReactPortal)) | null | undefined; ... 287 more ...; onTransitionEndCapture?: TransitionEventHandler<...> | undefined; }' is not assignable to type 'MakePathParamOptions<true | ParamsReducer<{} | {} | { id: string; } | ({ id: string; } & {}), {} | {} | ({ id: string; } & {})>>'.
    Types of property 'params' are incompatible.
      Type 'true | ((current: {} | {} | { id: string; } | ({ id: string; } & {})) => never) | undefined' is not assignable to type 'true | ParamsReducer<{} | {} | { id: string; } | ({ id: string; } & {}), {} | {} | ({ id: string; } & {})>'.
        Type 'undefined' is not assignable to type 'true | ParamsReducer<{} | {} | { id: string; } | ({ id: string; } & {}), {} | {} | ({ id: string; } & {})>'.

leqwasd avatar Mar 07 '24 14:03 leqwasd

@schiller-manuel did anything change here? LinkProps used to work just fine.

SeanCassiere avatar Mar 08 '24 01:03 SeanCassiere

Adding

{...toOptions}
from={"/"}

satisfies props.. But this will overwrite the "from" from the toOptions

leqwasd avatar Mar 11 '24 09:03 leqwasd

Not sure if it covers all your use-cases, but I was able to get a Link wrapper working with something like this. Personally I find it cleaner passing the whole route object as opposed to just the to. That also allowed me to get by without having to pass a generic type to my custom Link component since typescript can directly infer it from the route prop.

// helper types
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
type ExcludeEmpty<T> = T extends AtLeastOne<T> ? T : never;

// seem to be as safe/strict as the props of the tanstack-provided Link component
type RouteParams<T extends AnyRoute = RegisteredRouter['routeTree']> = ExcludeEmpty<
  T['types']['params']
> extends never
  ? { params?: never }
  : { params: T['types']['params'] };
type RouteSearch<T extends AnyRoute = RegisteredRouter['routeTree']> = ExcludeEmpty<
  T['types']['searchSchema']
> extends never
  ? { search?: never }
  : {
      search?: T['types']['searchSchema'] | ((args: T['types']['fullSearchSchema']) => T['types']['searchSchema']);
    };

// component props
type MyLinkProps<T extends AnyRoute = RegisteredRouter['routeTree']> = {
  route: T;
  children: React.ReactNode;
  className?: string;
  disabled?: boolean;
} & RouteParams<T> &
  RouteSearch<T>;

// custom Link component
function MyLink<T extends AnyRoute = RegisteredRouter['routeTree']>({
  route,
  children,
  ...other
}: MyLinkProps<T>) {
  return (
    <Link<AnyRoute> to={route?.to} {...other}>
      {children}
    </Link>
  );
}


As a note - there might be a better way to skin this cat, but the docs seem to be pretty limited on the topic. Just figured I would share what worked for me in case you were still stuck here.

jfehrman avatar Mar 13 '24 17:03 jfehrman

So turns out this is what works.

const MyLink = <
  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
  TFrom extends RoutePaths<TRouteTree> | string = string,
  TTo extends string = '',
  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
  TMaskTo extends string = '',
>(
  props: React.PropsWithoutRef<
    LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
      Omit<React.HTMLProps<'a'>, 'children' | 'preload'>
  >
) => <Link {...props} />

SeanCassiere avatar Mar 19 '24 12:03 SeanCassiere

can we make use of this in createLink?

I don't want to expose all of these generics as public API, otherwise we cannot modify them without causing breaking changes

schiller-manuel avatar Mar 19 '24 12:03 schiller-manuel

So did anyone inspected - what's wrong with ToOptions? In my optinion - that is a user friendly type to be used in this place!

leqwasd avatar Mar 19 '24 12:03 leqwasd

This code was working

type MenuItem = { label: string; route: LinkProps; }; const MENUS: MenuItem[] = [ { label: 'Test', route: { params: (old) => old, search: (old) => ({...old, test: true}), }, }];

But I updated from 1.16.6 to 1.20.1 and it does not work anymore. I got the error : Type '{ params: (old: never) => never; search: (old: {}) => { test: boolean }; }' is not assignable to type 'LinkProps'. Property 'to' is missing in type '{ params: (old: never) => never; search: (old: {}) => { test: boolean }; }' but required in type 'CheckPathError<Route<any, "/", "/", string, "__root__", RootSearchSchema, RootSearchSchema, RootSearchSchema, RootSearchSchema, ... 11 more ..., any>>'.ts(2322) link.d.ts(72, 5): 'to' is declared here. Test.tsx(14, 3): The expected type comes from property 'route' which is declared here on type 'MenuItem'

Maquinours avatar Mar 19 '24 14:03 Maquinours

I've seen many similar issues/discussions and stackoverflow posts. At this point, we could really use some official document / examples on how to wrap <Link> components in a type safe way. Even the customized solutions that work, only the to prop is typed but things like search and params do not get type safety that match to.

ziw avatar Mar 24 '24 05:03 ziw

Yeah, this is basically the same as #1194. I proposed contributing a solution but there was no response from the maintainers at that time.

Here's the workaround I use currently:

(It uses a wrapper function to create the link options, so it can be used as a regular object. This also works for passing it into a component without requiring a ton of boilerplate generics everywhere.)

export type RouterLinkProps = Parameters<RegisteredRouter["navigate"]>[0];

/**
 * Validate a router link as type-safe and return a generic {@link RouterLinkProps}.
 * @example link({ to: "/view/$id", params: { id: "1" } })
 */
export function link<
    TRouteTree extends AnyRoute = RegisteredRouter["routeTree"],
    TFrom extends RoutePaths<TRouteTree> | string = string,
    TTo extends string = "",
    TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
    TMaskTo extends string = "",
>(options: UseLinkPropsOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>) {
    return options as RouterLinkProps;
}

The type can also be spread directly:

const someLink = link({ .... });
...
function MyComponent({ someLink } : { someLink: RouterLinkProps }) {
	return <Link {...someLink} />;
}

jaens avatar Mar 24 '24 22:03 jaens

I tried to update my typings based on @jaens example, but unfortunately it does not work in 1.22.0. My version looks like this:

function MyLink<
  TRouteTree extends AnyRoute = RegisteredRouter["routeTree"],
  TFrom extends RoutePaths<TRouteTree> | string = string,
  TTo extends string = "",
  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
  TMaskTo extends string = "",
>({
  variant,
  testId,
  linkProps,
  children,
}: {
  variant?: StyledLinkProps["variant"];
  testId?: StyledLinkProps["testId"];
  linkProps: UseLinkPropsOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>;
  children?: ReactNode;
}) {
  return (
    <StyledLink variant={variant} testId={testId}>
      <Link {...linkProps}>{children}</Link>
    </StyledLink>
  );
}

I also tried this version by @SeanCassiere. This was interesting because my editor is happy but npm run build still breaks:

function MyLink<
  TRouteTree extends AnyRoute = RegisteredRouter["routeTree"],
  TFrom extends RoutePaths<TRouteTree> | string = string,
  TTo extends string = "",
  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
  TMaskTo extends string = "",
>({
  variant,
  testId,
  linkProps,
  children,
}: {
  variant?: StyledLinkProps["variant"];
  testId?: StyledLinkProps["testId"];
  linkProps: React.PropsWithoutRef<
    LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
      Omit<React.HTMLProps<'a'>, 'children' | 'preload'>
  >;
  children?: ReactNode;
}) {
  return (
    <StyledLink variant={variant} testId={testId}>
      <Link {...linkProps}>{children}</Link>
    </StyledLink>
  );
}

This previous version worked fine in 1.19.4:

function MyLink<
  TRouteTree extends AnyRoute = RegisteredRouter["routeTree"],
  TFrom extends RoutePaths<TRouteTree> | string = string,
  TTo extends string = "",
  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
  TMaskTo extends string = "",
>({
  variant,
  testId,
  linkProps,
  children,
}: {
  variant?: StyledLinkProps["variant"];
  testId?: StyledLinkProps["testId"];
  linkProps: LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & React.RefAttributes<HTMLAnchorElement>;
  children?: ReactNode;
}) {
  return (
    <StyledLink variant={variant} testId={testId}>
      <Link {...linkProps}>{children}</Link>
    </StyledLink>
  );
}

I've seen many similar issues/discussions and stackoverflow posts. At this point, we could really use some official document / examples on how to wrap components in a type safe way

@ziw 100% this. There is a clear need to create a custom wrapper for the provided Link component and it becomes a big problem if the typings of the library do not remain stable.

Jarzka avatar Mar 25 '24 13:03 Jarzka

I also tried this version by @SeanCassiere. This was interesting because my editor is happy but npm run build still breaks:

Not sure if this helps, but there's a variation of this, that I'm using that works.

const AppNavigationLink = <
  TRouteTree extends AnyRoute = RegisteredRouter["routeTree"],
  TFrom extends RoutePaths<TRouteTree> | string = string,
  TTo extends string = "",
  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
  TMaskTo extends string = "",
>(props: {
  name: string;
  props: Omit<
    React.PropsWithoutRef<
      LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
        Omit<React.ComponentPropsWithoutRef<"a">, "preload">
    >,
    "children" | "className" | "activeProps" | "inactiveProps"
  >;
}) => {
  const { name, props: linkProps } = props;
  return (
    <li>
      <Link
        className="..."
        activeProps={{ className: "..." }}
        inactiveProps={{ className: "..." }}
        {...linkProps}
      >
        {name}
      </Link>
    </li>
  );
};

Source

SeanCassiere avatar Mar 25 '24 21:03 SeanCassiere

I've seen many similar issues/discussions and stackoverflow posts. At this point, we could really use some official document / examples on how to wrap components in a type safe way

@ziw 100% this. There is a clear need to create a custom wrapper for the provided Link component and it becomes a big problem if the typings of the library do not remain stable.

We are working on this!

From what I can gather, most of the questions on the Discord questions channel and here related to this topic, is around having a type which can be applied into a custom component's props, to then later be applied on the TSR provided <Link> component.

import { Link, type SomeNewLinkPropsType } from "@tanstack/react-router";

function CustomLink({ linkProps } : { linkProps: SomeNewLinkPropsType }) {
  const { className, ...rest } = linkProps;
  return <Link className={`foo-bar ${className}`} {...rest} />
}

Once we figure out the correct story for the user-facing types for this, we'll make sure the documentation is updated to reflect the correct way forward.

SeanCassiere avatar Mar 25 '24 21:03 SeanCassiere

So I looked at createLink, and I think it works pretty much the way we want it to, regarding type-safety and everything. Here's a TS playground:

https://tsplay.dev/Wzq93m

I've tested with additional properties, searchParams validation etc, it all looks good. One issue is that createLink freezes the browser at runtime. No kidding, this was weird, but I have a PR open that fixes it.

Once that is merged, I think createLink is a good abstraction to use in user-land, because it's type-safe and you can compose over every component that you want.

Only requirement is that props are being spread onto an actual a tag. This is something that is not type-safe, and maybe something that we should document.

Let me know if there's anything I'm missing

TkDodo avatar Mar 26 '24 13:03 TkDodo

So I looked at createLink, and I think it works pretty much the way we want it to, regarding type-safety and everything. Here's a TS playground:

https://tsplay.dev/Wzq93m

ok, cool, that seems to mostly work..

One issue though is that once the component is wrapped in createLink it has some props which are set to undefined or an empty object. When deconstructing these into the component they overwrite any props set by the component (as intended, if the props were actually set by the consumer).

eg the className set in the example below is overwritten with undefined from the props.

const MyStyledRouterLink = createLink(MyStyledLink);

function MyStyledLink({ children, ...props }: React.ComponentProps<"a">) {
  return (
    <a className="foo-bar" {...props}>
      {children}
    </a>
  );
}

if I stringify & parse the props to remove the undefined it works as intended.

{...JSON.parse(JSON.stringify(props))}
Screenshot 2024-03-28 at 11 01 16 pm

lecstor avatar Mar 28 '24 13:03 lecstor

also.. I was pleasantly surprised when I initially used Link for external links and it all seemed to work fine. While testing createLink I noticed that it complained about the absolute url not being assignable, then I tested Link and it did the same so I thought I must have been tripping, but looking at the code it looks like it's intended to work, so I include the props I see for those as well..

Screenshot 2024-03-28 at 10 58 02 pm

and submit a PR which I believe resolves the props issue.. (but not the type error on to with absolute urls) https://github.com/TanStack/router/pull/1386

lecstor avatar Mar 28 '24 14:03 lecstor

It's probably not relevant anymore, but LinkProps broke for me in 1.17.5. It still worked in 1.17.4.

vixducis avatar Apr 04 '24 12:04 vixducis

I am using 1.32.5 and the custom component linkProps does throw some TS issues:

Type TRouteTree does not satisfy the constraint AnyRouter

I think this is related to the same issue, I had following code which does not satisfy TS any longer:

function TabbedNavigationItem<
  TRouteTree extends AnyRoute = RegisteredRouter["routeTree"],
  TFrom extends RoutePaths<TRouteTree> | string = string,
  TTo extends string = "",
  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
  TMaskTo extends string = ""
>({
  label,
  activeHref,
  linkProps
}: {
  linkProps: LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
    Omit<React.HTMLProps<"a">, "children" | "preload">
  label: string
  activeHref: LinkProps["to"]
}) {

dohomi avatar May 14 '24 05:05 dohomi

The weird TS issues I was having seems to be resolved in version 1.33.4. This worked for me, with the exception of having to add a ts-expect-error on the actual <Link/> component. Unsure exactly how to resolve it, but usage of this component works in practice.

// ...omitted
import type { LinkProps, RegisteredRouter, RoutePaths } from '@tanstack/react-router';
import { Link } from '@tanstack/react-router';

export type LinkButtonProps<
  TRouter extends RegisteredRouter = RegisteredRouter,
  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
  TTo extends string = '',
  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
  TMaskTo extends string = '',
> = Pick<LinkProps<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>, 'params' | 'search' | 'to'> &
  StrictOmit<ButtonProps, 'onClick'>;

export const LinkButton = <
  TRouter extends RegisteredRouter = RegisteredRouter,
  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
  TTo extends string = '',
  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
  TMaskTo extends string = '',
>({
  params,
  search,
  to,
  ...props
}: LinkButtonProps<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>) => {
  return (
    // @ts-expect-error Not sure what's causing this type error
    <Link params={params} search={search} to={to}>
      <Button {...props} />
    </Link>
  );
};

In practice:

// No type error
<LinkButton params={{ productId: "123" }} to="/app/product/$productId" label="Go to Product"   />

// Type error (expects params prop)
<LinkButton to="/app/product/$productId" label="Go to Product"   />

enaluz avatar May 19 '24 02:05 enaluz

Since creating a custom wrapper component for Link with working typing seems to be a) difficult, b) easy to break with version updates and c) not officially supported (not mentioned in the documentation), we decided to create our links using the official Link component, but by wrapping it with our own StyledLink component, which adds styling. It looks like this:

function StyledLink({ children, testId }: StyledLinkProps) {
  const childrenWithClassName = Children.map(children, (child) =>
    cloneElement(child, {
      className: classes.styledLink,
      "data-testid": testId,
    }),
  );

  return <>{childrenWithClassName}</>;
}

We use it like this:

<StyledLink testId="myAnchor">
  <Link
    to={routeToSeppo}
    params={seppoParams}
  >
    Seppo Taalasmaa
  </Link>
</StyledLink>

One downside with this approach is that we need to remember to manually wrap every Link with StyledLink. Still, I hope this solution is more stable since we do not have any custom typing logic.

Jarzka avatar May 22 '24 12:05 Jarzka

@Jarzka did you try out createLink? we are still gathering feedback about this before we promote it from experimental

schiller-manuel avatar May 22 '24 17:05 schiller-manuel

didn't know that createLink is experimental but really, it works great on type level and on runtime (since I fixed it 😂). Also no type parameters needed in user-land. All those solutions with 5 type parameters are way too complicated for app level code.

Here's what I do:

import { Button } from 'my-design-system'

const ButtonWrapper = (props: Omit<React.ComponentProps<typeof Button>, 'as'>) => <Button as="a" {...props} />

export const RouterButton = createLink(ButtonWrapper)

that's it 🤷 . Now I can do:

<RouterButton to="/my/route/" params={{ this: 'is', typesafe: true }}>Hello</RouterButton>

everything is styled like my normal <Button as="a"> and we get type-safe to navigation. What's not to love :)

TkDodo avatar May 22 '24 18:05 TkDodo

@TkDodo how do you apply activeProps without having to pass them every time?

I want to be able to achieve something like this:

import { ListItem } from 'my-design-system';

function NavListItem(props: ButtonProps & LinkProps) {
    return <ListItem as={Link} {...props} activeProps={{ className: 'ListItem-active' }} />;
}

<NavListItem to="/my/route" params={{ foo: 'bar' }}>
  My Link
</NavListItem>

tobyzerner avatar May 23 '24 00:05 tobyzerner

@Jarzka did you try out createLink? we are still gathering feedback about this before we promote it from experimental

Yes, I am aware of createLink, but we did not want to use it for a few reasons:

  • We are slowly heading towards production and don't want to use experimental / undocumented features at this point
  • I have bad memories of the removal of buildLink during the beta period as it caused us to do massive refactoring for almost all links.
  • We want to maintain the possibility to switch the library in case of emergency, so we do not want to rely too much on TanStack specific features.

Jarzka avatar May 23 '24 05:05 Jarzka

@TkDodo how do you apply activeProps without having to pass them every time?

Haven't done this yet since I only want this passed for our sideBar navigation, where I pass it manually. But you should be able to do one more layer of nesting:

import { ListItem } from 'my-design-system'

const ListItemWrapper = (props: Omit<React.ComponentProps<typeof ListItem>, 'as'>) => <ListItem as={Link} {...props} />

const CreatedNavListItem = createLink(ListItemWrapper)

export const NavListItem: LinkComponent<typeof ListItemWrapper> = (props) => (
  return <CreatedNavListItem {...props} activeProps={{ className: 'ListItem-active' }} />
)

TkDodo avatar May 23 '24 06:05 TkDodo

And what about ToOptions type? What is the purpose of it? Where is it suppose to be used? Maybe it shouldn't even be exported

leqwasd avatar May 23 '24 07:05 leqwasd

And what about ToOptions type? What is the purpose of it? Where is it suppose to be used? Maybe it shouldn't even be exported

The ToOptions type mostly comes in handy when you need access to the inference of the Router's available routes. For custom functions, etc... It's delicate since it needs access to generics to work correctly, and it's something that can certainly change as we push for better ts-performance.

It's not a magic bullet type for use with <Link> or useNavigate(), rather it's for more fine-grained types.

SeanCassiere avatar May 23 '24 07:05 SeanCassiere

@TkDodo Thanks this kind of worked:

import { ListItem } from 'my-design-system'

const ListItemWrapper = (props: Omit<React.ComponentProps<typeof ListItem>, 'as'>) => <ListItem as={Link} {...props} />

const CreatedNavListItem = createLink(ListItemWrapper)

export const NavListItem: LinkComponent<typeof ListItemWrapper> = (props) => (
  return <CreatedNavListItem {...props} activeProps={{ className: 'ListItem-active' }} />
)

I have another use-case where I would like to use a isSelected prop on the component as activeProps. Seems the types for the following break though:

export const NavListItem: LinkComponent<typeof ListItemWrapper> = (props) => (
  return <CreatedNavListItem {...props} activeProps={{ isSelected:true }} inactiveProps={{ isSelected:false }}/>
)

Would it be possible to override activeProps and inactiveProps types when using createLink API? something like:

type ActiveLinkAnchorProps<TComp> = Omit<React.AnchorHTMLAttributes<HTMLAnchorElement> & {
    [key: `data-${string}`]: unknown;
}, 'children'> & React.ComponentProps<TComp>;

computnik avatar Sep 02 '24 15:09 computnik