oedotme / generouted

Generated file-based routes for Vite
https://stackblitz.com/github.com/oedotme/generouted/tree/main/explorer
MIT License
1.02k stars 47 forks source link

How to extends Link component from router file #150

Closed FrancoRATOVOSON closed 3 months ago

FrancoRATOVOSON commented 6 months ago

Sorry, discussion is not enabled in this repos so I opened an issue.

Context

I shadcn-ui for my app and I style my links the way it is explained in the (doc)[https://ui.shadcn.com/docs/components/button#as-child], here's the code :

import { Link as RouterLink } from 'react-router-dom'

import { Button, buttonVariants } from 'ui/components'
import { VariantProps } from 'ui/utils'

interface LinkProps
  extends React.ComponentProps<typeof RouterLink>,
    VariantProps<typeof buttonVariants> {}

function Link({ size, variant = 'link', ...props }: LinkProps) {
  return (
    <Button asChild size={size} variant={variant}>
      <RouterLink {...props} />
    </Button>
  )
}

As you can see, I had to import the link component from react-router-dom, because when I use the one provided by the route.ts file like this :

import { Button, buttonVariants } from 'ui/components'
import { VariantProps } from 'ui/utils'

import { Link as RouterLink } from '../router'

interface LinkProps
  extends React.ComponentProps<typeof RouterLink>,
    VariantProps<typeof buttonVariants> {}

function Link({ size, variant = 'link', ...props }: LinkProps) {
  return (
    <Button asChild size={size} variant={variant}>
      <RouterLink {...props} />
    </Button>
  )
}

I get a typescript error with the {...props}

Cannot assign '{}' to the type 'IntrinsicAttributes & (LinkProps & ({ to: "/"; params?: undefined; } | { to: "/customers"; params?: undefined; } | { to: "/customers/:id"; params: { id: string; }; } | ... 5 more ... | { ...; }))'

And yes, when I tried to see, if I : const props: React.ComponentProps<typeof RouterLink>, the type of props is indeed { }.

So how can I extends my prop type to match the prop type of this component ?

oedotme commented 6 months ago

Hey @FrancoRATOVOSON, could you please share a StackBlitz example with your usage of shadcn/ui and generouted? I'd like to try adding the types in this context and make sure it works.

FrancoRATOVOSON commented 6 months ago

Hey @FrancoRATOVOSON, could you please share a StackBlitz example with your usage of shadcn/ui and generouted? I'd like to try adding the types in this context and make sure it works.

https://stackblitz.com/edit/vitejs-vite-edpmge?file=src%2Fcomponents%2Flink.tsx

oedotme commented 6 months ago

@FrancoRATOVOSON I've exported helper types to simplify the custom usage. I tried to access the StackBlitz demo, but seems broken at the moment — here's an example:

import { Link as RouterLink } from 'react-router-dom'
import { LinkProps } from '@generouted/react-router/client'

import { Params, Path } from '../../src/router'

type VariantProps = { variant: 'solid' | 'outline' }

export function Link<P extends Path>({ variant, ...props }: LinkProps<P, Params> & VariantProps) {
  return (
    <Button variant={variant}>
      <RouterLink {...props} />
    </Button>
  )
}

;<Link to="/login" variant="solid">Login</Link>
;<Link to="/not-valid">Not valid</Link> // type-error

Hope that helps.

braydenbabbitt commented 5 months ago

Just used this to add a NakedLink component in my project that extends Link and adds css to remove default anchor styles. Super helpful! Thanks!

FrancoRATOVOSON commented 5 months ago

@FrancoRATOVOSON I've exported helper types to simplify the custom usage. I tried to access the StackBlitz demo, but seems broken at the moment — here's an example:

import { Link as RouterLink } from 'react-router-dom'
import { LinkProps } from '@generouted/react-router/client'

import { Params, Path } from '../../src/router'

type VariantProps = { variant: 'solid' | 'outline' }

export function Link<P extends Path>({ variant, ...props }: LinkProps<P, Params> & VariantProps) {
  return (
    <Button variant={variant}>
      <RouterLink {...props} />
    </Button>
  )
}

;<Link to="/login" variant="solid">Login</Link>
;<Link to="/not-valid">Not valid</Link> // type-error

Hope that helps.

This doesnt consider params and just past the to given as props.

oedotme commented 5 months ago

@FrancoRATOVOSON I'm not sure I understand what you mean, could you please elaborate on that?

Also, would be helpful if you provide a StackBlitz example as the previous one you shared seems broken.

briandunn commented 4 months ago

@oedotme I wonder if you can provide an example of using the Link from the generated router file and extending that? I like the way that component takes a Path and Params, but I can't figure out how to customize it.

TLDR: Typescript error on line 20 https://stackblitz.com/edit/github-oenxul?file=src%2Fpages%2F_app.tsx

example:

import { Link, Path, Params } from "@/router";
import classNames from "classnames";
import type { LinkProps } from "@generouted/react-router/client";

export type NavigationItemProps<P extends Path> = LinkProps<P, Params> & {
  active?: boolean;
  linkClass?: string;
  label: string;
  icon: React.ReactNode;
};

export default function OpenNavigationItem<P extends Path>({ active = false, params, linkClass, icon, to }: NavigationItemProps<P>) {
  return (
    <Link
      params={params}
      to={to}
      className={classNames(
        "hover:!bg-gray-200 flex justify-center items-center h-14 w-14 rounded-full",
        { "bg-sys-brand-secondary-container": active },
        linkClass
      )}
    >
      {icon}
    </Link>
  );
}

I expected this to work, since it's just extending the LinkProps type. instead I get:

error TS2322: Type '{ children: ReactNode; params: { sectionId: string; } | { sectionId: string; } | undefined; to: "/" | "/a" | "/b" | "/c" | ... 6 more ... | "/d/:id"; className: string; }' is not assignable to type 'IntrinsicAttributes & LinkProps<"/" | "/a" | "/b" | "/c" | "/d" | ... 5 more ... | "/e/:id", Params>'.
    Types of property 'to' are incompatible.
        Type '"/"' is not assignable to type '"/d/:id"'.

Sorry route names are obfuscated.

oedotme commented 4 months ago

@briandunn @FrancoRATOVOSON As the params prop is conditional, we'd need to pass all link props grouped.

Following the shorter example you've provided, this would fail:

const PrettyLink = <P extends Path>({ to, params }: LinkProps<P, Params>) => (
  <Link to={to} params={params} data-pretty />
)

We should instead pass all props directly:

const PrettyLink = <P extends Path>({ ...props }: LinkProps<P, Params>) => (
  <Link {...props} data-pretty />
)


To combine other props with the Link props — as the longer example, it seems that TypeScript infers props differently when you destructure other props before ...props:

type Props = { active: boolean }

const PrettyLink = <P extends Path>({ active, ...props }: Props & LinkProps<P, Params>) => {
  return <Link {...props} data-pretty />
}

In this case instead, we can either use props.active directly (which is my preference, that's why I haven't encounter issues extending the props 🙂) or destructure the external props later in the component:

type Props = { active: boolean }

const PrettyLink = <P extends Path>(props: Props & LinkProps<P, Params>) => {
  // props.active | const { active } = props
  return <Link {...props}  data-pretty />
}

<PrettyLink to="/posts/:id" params={{ id: "xyz" }} />  ✅

Third possibility would be to allocate a namespace for original link props:

type Props = { active: boolean }

const PrettyLink = <P extends Path>({ active, link }: Props & { link: LinkProps<P, Params> }) => {
  return <Link {...link} data-pretty />
}

<PrettyLink link={{ to: '/posts/:id', params: { id: 'xyz' } }} />  ✅

Hope that helps!

oedotme commented 3 months ago

Closing for inactivity. Feel free to reopen if you guys still have problems with the types.