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

useLoaderData<typeof loader>() returns incorrect type #4529

Closed
andrecasal opened this issue Nov 6, 2022 · 14 comments
Closed

useLoaderData<typeof loader>() returns incorrect type #4529

andrecasal opened this issue Nov 6, 2022 · 14 comments

Comments

@andrecasal
Copy link

andrecasal commented Nov 6, 2022

What version of Remix are you using?

1.7.5

Steps to Reproduce

In the following code, the events variable has the wrong type:

import { json } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"
import type { Event } from "@prisma/client"

import { getEvents } from "~/models/events.server"

export const loader = async () => json({ events: await getEvents() })

const Events = () => {
  const { events } = useLoaderData<typeof loader>()

  return (
    <>
      <h1>Events</h1>
      {events.map(event => <Event key={event.id} event={event} />)}
    </>
  )
}

const Event = ({ event }: { event: Event }) => {
  const { title, start, end } = event
  return <p>{event.title}</p>
}

export default Events

Screen Shot 2022-11-06 at 15 04 24

yielding an error on the map() function:
Screen Shot 2022-11-06 at 17 21 34

Steps to reproduce

  1. Copy this code into an events.tsx route
import { json } from "@remix-run/node"
import { useLoaderData } from "@remix-run/react"
import type { Event } from "@prisma/client"

export const loader = async () => json({ events: [
  {id: 'adasdasd', title: "Event 1", start: "day 1", end: "day 2"},
  {id: 'qweqweqw', title: "Event 2", start: "day 2", end: "day 3"},
] })

const Events = () => {
  const { events } = useLoaderData<typeof loader>()

  return (
    <>
      <h1>Events</h1>
      {events.map(event => <Event key={event.id} event={event} />)}
    </>
  )
}

const Event = ({ event }: { event: Event }) => {
  const { title, start, end } = event
  return <p>{event.title}</p>
}

export default Events
  1. Hover over events to see the type

I'll create a test once this bug's confirmed.

Expected Behavior

I'd expect useLoaderData() to give me the type:

Event[]

or on the example with a hardcoded array:

{
  id: string,
  title: string,
  start: string,
  end: string
}

Actual Behavior

useLoaderData<typeof loader>() type is SerializeObject<UndefinedToOptional<Event>>[]

@machour
Copy link
Collaborator

machour commented Nov 6, 2022

Hey @andrecasal, I would recommend upgrading your Remix packages to at least 1.6.5, where we enhanced type inference from loader and action: https://github.com/remix-run/remix/releases/tag/remix%401.6.5

Closing this as it's already fixed.

@machour machour closed this as not planned Won't fix, can't repro, duplicate, stale Nov 6, 2022
@andrecasal
Copy link
Author

Awesome, thanks!

@andrecasal
Copy link
Author

andrecasal commented Nov 6, 2022

I just realized the version I'm using 1.7.5 for @remix-run/node and @remix-run/react.

I mistakenly looked at the 1.6.4 for the dev dependencies @remix-run/dev, @remix-run/eslint-config, and @remix-run/serve.

json() and useLoaderData() come from @remix-run/node and @remix-run/react respectively, so this might still be a problem.

@machour
Copy link
Collaborator

machour commented Nov 6, 2022

@andrecasal make sure you use the same version for all packages, or you might have some inconsistent behaviors.

As for the loader, you need to hint its parameter with LoaderArgs for type inference to work:

export async function loader(args: LoaderArgs) {
   return json(data);
}

or

export async function loader({request}: LoaderArgs) {
  // do your thing with request 
   return json(data);
}

@andrecasal
Copy link
Author

andrecasal commented Nov 6, 2022

All packages (dev and otherwise) have been updated to 1.7.5 and I've added the {request}: LoaderArgs to the loader function, but the problem still persists. Here's my package.json:

{
	"name": "andrecasal.com",
	"private": true,
	"description": "",
	"license": "",
	"sideEffects": false,
	"scripts": {
		"dev": "remix dev",
		"build": "remix build",
		"start": "remix-serve api"
	},
	"prisma": {
		"seed": "ts-node prisma/seed.ts"
	},
	"dependencies": {
		"@prisma/client": "^4.5.0",
		"@remix-run/node": "^1.7.5",
		"@remix-run/react": "^1.7.5",
		"@remix-run/vercel": "^1.7.5",
		"@vercel/node": "^2.6.1",
		"front-matter": "^4.0.2",
		"marked": "^4.2.2",
		"react": "^18.2.0",
		"react-dom": "^18.2.0",
		"remark-toc": "^8.0.1",
		"styled-components": "^5.3.6",
		"tiny-invariant": "^1.3.1"
	},
	"devDependencies": {
		"@remix-run/dev": "^1.7.5",
		"@remix-run/eslint-config": "^1.7.5",
		"@remix-run/serve": "^1.7.5",
		"@types/marked": "^4.0.7",
		"@types/node": "^18.11.9",
		"@types/react": "^18.0.25",
		"@types/react-dom": "^18.0.8",
		"@types/styled-components": "^5.1.26",
		"eslint": "^8.27.0",
		"prisma": "^4.5.0",
		"ts-node": "^10.9.1",
		"typescript": "^4.8.4"
	}
}

And the events.tsx route:

...
export const loader = async ({ request }: LoaderArgs) => json({ events: await getEvents() })

const Events = () => {
	const { events } = useLoaderData<typeof loader>()

	const eventsComponent = events.length > 0 ? events.map((e) => <Event key={e.id} event={e} />) : <NoScheduledEvents />

	return (
		<CenterContent>
			<h1>Events</h1>
			{eventsComponent}
		</CenterContent>
	)
}
...

Still showing SerializeObject<UndefinedToOptional<Event>>[]:
Screen Shot 2022-11-06 at 23 10 54

@machour
Copy link
Collaborator

machour commented Nov 6, 2022

Please try the function form as mentioned in the release note.

export async function loader({request}: LoaderArgs) {
    return json({ events: await getEvents() });
}

@MichaelDeBoey
Copy link
Member

@machour Using both function or arrow function should work here.

I think the problem is Event has some properties that are changing types when serializing them (for instance Date that becomes string)

@andrecasal
Copy link
Author

andrecasal commented Nov 7, 2022

@MichaelDeBoey you're right start and end where being declared as DateTime in the Prisma schema:

model Event {
   id               Int      @id @default(autoincrement())
   title            String   @unique
   type             String
   image            String
   start            DateTime
   end              DateTime
   description      String
   location         Location @relation(fields: [locationId], references: [id])
   locationId       Int
   streamLink       String?
   registrationLink String
   price            Int
}

model Location {
   id      Int     @id @default(autoincrement())
   name    String  @unique
   address String
   lat     Float
   lng     Float
   Event   Event[]
}

The Prisma client returns all DateTime as native Date objects, which breaks the type checking when communicating values from the backend to the frontend.

I changed start and end to String for now and it worked 👌

Thank you for your help guys!

@andrecasal
Copy link
Author

Out of curiosity, how would you deal with DateTimes In Prisma?

It's saved as a String in the DB anyway, but the client converts them to Date objects.

Do you avoid DateTimes entirely or convert Date objects back to strings before sending them off to the frontend?

@kiliman
Copy link
Collaborator

kiliman commented Nov 8, 2022

Checkout remix-typedjson which was written spefically to handle non-JSON types. You get full type inferrence and type fidelity.

https://github.com/kiliman/remix-typedjson

@andrecasal
Copy link
Author

Excellent work Kiliman, thank you 😄✌️

@andrecasal
Copy link
Author

Is adopting typedjson in Remix's roadmap and making this transparent for the developer?

@machour
Copy link
Collaborator

machour commented Nov 9, 2022

@andrecasal I don't think it's the case.
You can open a discussion (proposal category) to see how much traction this idea gets.

@kiliman
Copy link
Collaborator

kiliman commented Nov 9, 2022

Yeah I don't think every feature must be included in Remix core. I like that the Remix API gives you nice building blocks that you can put together as needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants