vercel / next.js

The React Framework
https://nextjs.org
MIT License
127.03k stars 26.99k forks source link

Incorrect warning for objects with null prototype passed from Server Components to Client Components #47447

Open charltoons opened 1 year ago

charltoons commented 1 year ago

Verify canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: x64
      Version: Darwin Kernel Version 22.3.0: Mon Jan 30 20:38:37 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T6000
    Binaries:
      Node: 16.19.0
      npm: 9.4.1
      Yarn: 1.22.19
      pnpm: 7.14.2
    Relevant packages:
      next: 13.2.5-canary.14
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true)

Link to the code that reproduces this issue

https://github.com/charltoons/next-rsc-null-prototype-warning

To Reproduce

Pass an object with a null prototype from a Server Component to a Client Component

Example server component:

  const objectWithNullPrototype = Object.create(null);
  objectWithNullPrototype.foo = "bar";
  return <ClientComponent myObject={objectWithNullPrototype} />

Example client component:

"use client";

export const ClientComponent = (myObjectProp: any) => {
  return <pre>{JSON.stringify(myObjectProp)}</pre>;
};

Describe the Bug

Observe in the server logs the following warning:

Warning: Only plain objects can be passed to Client Components from Server Components. Classes or other objects with methods are not supported.
  <... myObject={{foo: "bar"}}>
                ^^^^^^^^^^^^^^

While this is only a warning and does not inhibit runtime behavior, large objects will print this warning for each object instance. We discovered this issue when trying to pass an extracted cache from an Apollo client (which uses null prototype) to a client component. In this case, it results in hundreds of lines of warning logs for each request.

I'm not sure if this a a Next.js bug or a React bug. The issue can be attributed to this function: https://github.com/vercel/next.js/blob/canary/packages/next/src/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-server.browser.development.js#L1154

Expected Behavior

No warning is emitted since objects with null prototype are serializable.

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

Vercel (the issue appears during local dev)

balazsorban44 commented 1 year ago

This comes from React, we just vendor it for use in the App Router https://github.com/facebook/react/blob/9c54b29b44d24f8f8090da9c7ebf569747a444df/packages/shared/ReactSerializationErrors.js#L28

leikoilja commented 1 year ago

is there an easy way to silence that warning? 😅 Edit: sadly will have to settle for JSON.parse(JSON.stringify(data)) from https://github.com/vercel/next.js/issues/11993#issuecomment-617375501 for now

emondpph commented 1 year ago

Having same issue for records coming from Mysql database

Jan-T-Berg commented 1 year ago

Can i deactivate this warning? I got many of it...

Tommoore96 commented 1 year ago

It's happing for me with files, which I can't stringify 😬

I can upload everything fine, but get this huge red warning.

      const formData = new FormData();
      formData.append("audioFile", input.audioFile[0] as File);
          await upload(
            formData,
          );

Looks like I'm doing everything correctly too.

kob490 commented 1 year ago

+1 for this. seem like it can be resolved with JSON.parse(JSON.stringify(data)) but this is reduntant.

ng9000 commented 1 year ago

Passing mongo db objectId's also gives this error, stringifying it gives more errors and it becomes repetitive. I just wanted to know if this warning can create any huge errors?

amirrasooli69 commented 1 year ago

you shoud before passing data to page for use data, convert to string and so to json data={JSON.parse(JSON.stringify(unit))}

Abdul-Samii commented 11 months ago

I'm still getting this error. Can anyone help me out?

jixboa commented 11 months ago

Am still getting this error. Any solution?

tomskip123 commented 11 months ago

I started getting this issue using react query, I was trying to use the provider in the server component.

moving it into its own 'providers.tsx' and using "use client"; at the top solved this for me

ahmetkca commented 10 months ago

It's happing for me with files, which I can't stringify 😬

I can upload everything fine, but get this huge red warning.

      const formData = new FormData();
      formData.append("audioFile", input.audioFile[0] as File);
          await upload(
            formData,
          );

Looks like I'm doing everything correctly too.

I am having the same problem? were you able to fix it?

amannn commented 10 months ago

I have the same issue since graphql uses Object.create(null) (repro).

VaibhavKambar7 commented 10 months ago

I started getting this issue using react query, I was trying to use the provider in the server component.

moving it into its own 'providers.tsx' and using "use client"; at the top solved this for me

hey , were you able to fix it?

insurerity commented 9 months ago

I am experiencing this issue however it's not just throwing a warning in my server, I am unable to carry out mutations.

Screenshot 2024-02-19 140629

I have commented out more than half the code, still can't seem to find the problem.

werkamsus commented 8 months ago

I started getting this issue using react query, I was trying to use the provider in the server component.

moving it into its own 'providers.tsx' and using "use client"; at the top solved this for me

issue also comes up for me - using nextjs 14.1 and react-query 5.22. Provider placement sadly does not change it, and serialization breaks types, as well as parsing special objects like dates.

App seems to be working fine, is there any way to silence the warning?

ayrtoneverton commented 7 months ago

This problem is generated by the fact that you are passing a complex property from a server-side component to a client-side component, most likely at some level of the object there are classes or functions that are not easily serialized, so you need make the conversion manually before passing this property.

Example:

const getData = async () => AnyORM.find();

export const MyServerSideComponent = async () => {
  const data = await getData();

  return <MyClientSideComponent data={structuredClone(data)} />;
};

Doc: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

voinik commented 7 months ago

Problem

I ran into the same issue. I'm using libsql to fetch data from the database (Turso). I looked through the React code linked in the first response post above. It turns out you can't pass instances of classes; you can only pass plain objects. For me the issue was that libsql is adding 2 extra properties on top of the data queried, both of which they made non-enumerable. The React code actually goes through all the data you pass (here), goes through each property on each object, and checks if each property is enumerable. If it isn't, then you'll get the warning described in the OP.

I describe the issue in more detail in the Turso/libsql Discord here.

This issue only occurs in development. Not in prod!

Workarounds

Two workarounds have already been mentioned: JSON.parse(JSON.stringify(data)) and structuredClone(data). I don't like these, because they deep clone all of your data. If you do this for big chunks of data (which will probably happen a lot), it will result in a lot of overhead.

If your problem is also that your ORM/db client is adding non-enumerable properties, then all you need to do is spread the data into a new object: const newData = { ...data }. So if you get an array of objects back, you can map over it:

const newData = data.map(d => ({ ...d }));

If the non-enumerated properties only exist at the top level and you have nested data of some kind, then you will save yourself a lot of computations. Spreads only copy the top level of the object, not deeper nested data. The other two workarounds do deep copies.

If you have non-enumerated properties on each level, then you're better off doing a deep copy with structuredClone(data).

My structural solution

I use Kysely in my apps, and Kysely accepts plugins that alter queries or results gotten from the db. Instead of doing spread calls everywhere I use my db client, I wrote a plugin that does this for me. I'm sure other ORMs/query builders provide similar functionality. Here's the plugin I wrote:

import { type KyselyPlugin } from 'kysely';

export const spreadDataInDevPlugin: KyselyPlugin = {
    transformQuery: args => args.node,
    transformResult: async args => {
        // libsql adds some non-enumerable properties (length, and an index) to db responses.
        // React checks all properties in objects that are passed down from server- to client components.
        // To prevent devs from passing down non-trivial objects and possibly getting unexpected results,
        // they display a warning when a property is non-enumerable.
        // This only happens during development. See: https://github.com/vercel/next.js/issues/47447#issuecomment-2064362712
        // In order to avoid that warning for data fetched using libsql, we use this plugin
        // to spread the result, which copies over enumarable properties only.
        // We load the plugin in development only to prevent unnecessary computations.
        return { ...args.result, rows: args.result.rows.map(r => ({ ...r })) };
    },
};

Then we use it like this in our Kysely client setup:

const plugins = env.NODE_ENV === 'development' ? [spreadDataInDevPlugin] : [];

export const db = new Kysely<Database>({
    dialect: conn, // The dialect set up by yourself
    plugins,
});

~This runs only in dev, and the overhead in prod is minimal (just the env check, and the extra overhead from having callbacks from the plugin).~ We only load the plugin in dev, which means there is no overhead in prod to work around this. The added benefit is we still get the warnings React provides us if we pass down data that does not come from our ORMs/query builders! The con here is that we do slightly different things to our data in dev compared to other envs, so beware of that!

Hope this helps someone out there!

Edit: Changed the code to only load the plugin in dev, instead of always loading it and doing the env check inside the plugin

waqas-on-github commented 6 months ago

`'use client' import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { Textarea } from "./ui/textarea"; import { usePetContext } from "@/lib/hooks"; import { PetFormBtn } from "./pet_form_btn"; import { addPet, editPet } from "@/server_actions/actions"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { petFormSchem } from "@/lib/schemas";

export const PetForm = ({ actionType, checkFormOpen }: PetFormProps) => {

//get actionType handler form state manager 
const { selectedPet } = usePetContext()

const { formState: { errors, isSubmitting }, register, trigger, getValues } = useForm<Omit<petType, "id">>(
    { resolver: zodResolver(petFormSchem) }
)

const data: Omit<petType, "id"> = getValues()

// "client action"  for server action 
async function submitForm() {

    let responce = await trigger()
    if (!responce) return null

    if (actionType === "add") {
        try { 

            const res = await addPet(JSON.parse(JSON.stringify(data).toString()))
        res && checkFormOpen(false)

    } catch (error) {
        console.log(error);

            alert(`${error}`)
            // checkFormOpen(false)
    }

}

    if (actionType === "edit") {
        try {
            if (selectedPet) {

                const res = await editPet(selectedPet?.id, JSON.parse(JSON.stringify(data)))
                res && checkFormOpen(false)

            }

        } catch (error) {
            console.log(error);

            alert(`${error}`)
            checkFormOpen(false)
        }
    }
}

return (
    <form action={submitForm} className="flex flex-col">
        <div className="space-y-4 ">
            <div className="space-y-2">
                <Label htmlFor="name">Name</Label>
                <Input
                    id="name"
                    {...register("name", { required: "name is", maxLength: 20 })}
                />
                {errors.name && <p className="text-red-700" >{errors.name.message}</p>}
            </div>

            <div className="space-y-2">
                <Label htmlFor="Owner Name">Owner Name</Label>
                <Input
                    id="ownerName"
                    {...register("ownerName")}
                />
                {errors.ownerName && <p className="text-red-700">{errors.ownerName.message}</p>}

            </div>

            <div className="space-y-2">
                <Label htmlFor="Image url">Image url</Label>
                <Input
                    id="imageUrl"
                    {...register("imageUrl")}
                />
                {errors.imageUrl && <p className="text-red-700">{errors.imageUrl.message}</p>}

            </div>

            <div className="space-y-2">
                <Label htmlFor="Age">Age</Label>
                <Input
                    id="age"
                    {...register("age")}

                />
                {errors.age && <p className="text-red-700">{errors.age.message}</p>}
            </div>

            <div className="space-y-2">
                <Label htmlFor="Notes">Notes</Label>
                <Textarea id="notes"
                    {...register("notes")}
                />
                {errors.notes && <p className="text-red-700">{errors.notes.message}</p>}
            </div>
        </div>
        <PetFormBtn actionType={actionType} />
    </form>
);

}

` tried solutions but still getting same warning any other solution ?

voinik commented 6 months ago

@waqas-on-github It seems unlikely to me that the warning is triggered because of the form. Your form seems to contain only strings and perhaps a number for your “age” field. I’ve also never had this problem with form actions.

My guess is the problem is triggered from the fetching of your data. It looks like you’re keeping the pet data in a context (selectedPet)?

When passing data down from server components to client components, make sure to run {…obj} on every object in the list. For example:

const data = await db.getAllPets();
return data.map((pet) => ({…pet}));

Or

const data = await db.getOnePet(id);
return {…data};

ORMs or db clients tend to modify each object by adding certain non-iterable properties, which aren’t allowed by React when passing data down to client components. Calling {…obj} or in the worst case JSON.parse(JSON.stringify(obj)) will remove those properties and avoid the warning.

DarkShark-RAz commented 3 weeks ago

It's happing for me with files, which I can't stringify 😬

I can upload everything fine, but get this huge red warning.

      const formData = new FormData();
      formData.append("audioFile", input.audioFile[0] as File);
          await upload(
            formData,
          );

Looks like I'm doing everything correctly too.

Having same problem have you found a solution?