blitz-js / next-superjson-plugin

SuperJSON Plugin for Next.js Pages and Components
200 stars 13 forks source link

Each child in a list should have a unique "key" prop. #70

Open AmazingTurtle opened 1 year ago

AmazingTurtle commented 1 year ago

Verify Next.js canary release

Describe the bug

I see the Each child in a list should have a unique "key" prop. warning message on the client side on multiple occassions, basically every I figured out, that is, because I'm feeding the data to the react context api.

    <DocumentListContextProvider
      documents={documents}
      data-superjson
    >
        <div />
        <SomeServerComponentSvg />
        <SomeClientComponentAccessingContext />
    </DocumentListContextProvider>

Which components are affected? Apparently the warning appears for every node where there are more than 1 children. That goes for normal components as well as plain jsx.

A full error log:

hydration-error-info.js?7847:27 Warning: Each child in a list should have a unique "key" prop. See https://reactjs.org/link/warning-keys for more information.
    at div
    at DocumentListContextProvider (webpack-internal:///(app-client)/./app/(index)/documents/document-list-context.tsx:25:11)
    at WithSuperJSON (webpack-internal:///(app-client)/../../node_modules/next-superjson-plugin/dist/tools.js:153:55)
    at SuperJSONComponent (webpack-internal:///(app-client)/../../node_modules/next-superjson-plugin/dist/client.js:18:24)
    at ScrollAndFocusHandler (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:153:1)
    at InnerLayoutRouter (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:195:11)
    at RedirectErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:361:9)
    at RedirectBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:368:11)
    at NotFoundBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:404:11)
    at LoadingBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:317:11)
    at ErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/error-boundary.js:72:11)
    at RenderFromTemplateContext (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/render-from-template-context.js:12:34)
    at OuterLayoutRouter (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:23:11)
    at div
    at ScrollAndFocusHandler (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:153:1)
    at InnerLayoutRouter (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:195:11)
    at RedirectErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:361:9)
    at RedirectBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:368:11)
    at NotFoundBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:404:11)
    at LoadingBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:317:11)
    at ErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/error-boundary.js:72:11)
    at RenderFromTemplateContext (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/render-from-template-context.js:12:34)
    at OuterLayoutRouter (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:23:11)
    at div
    at div
    at ScrollAndFocusHandler (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:153:1)
    at InnerLayoutRouter (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:195:11)
    at RedirectErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:361:9)
    at RedirectBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:368:11)
    at NotFoundErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:397:9)
    at NotFoundBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:404:11)
    at LoadingBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:317:11)
    at ErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/error-boundary.js:72:11)
    at RenderFromTemplateContext (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/render-from-template-context.js:12:34)
    at OuterLayoutRouter (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/layout-router.js:23:11)
    at body
    at html
    at ReactDevOverlay (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/react-dev-overlay/internal/ReactDevOverlay.js:61:9)
    at HotReload (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/react-dev-overlay/hot-reloader-client.js:20:11)
    at Router (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/app-router.js:48:11)
    at ErrorBoundaryHandler (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/error-boundary.js:59:9)
    at ErrorBoundary (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/error-boundary.js:72:11)
    at AppRouter (webpack-internal:///(app-client)/../../node_modules/next/dist/client/components/app-router.js:24:13)
    at ServerRoot (webpack-internal:///(app-client)/../../node_modules/next/dist/client/app-index.js:147:11)
    at RSCComponent
    at Root (webpack-internal:///(app-client)/../../node_modules/next/dist/client/app-index.js:164:11)

I am using the 13.2.4-canary.8 version of next.

Expected behavior

I expect to see no warning about missing unique key props

Reproduction link

No response

Version

0.5.6

Config

module.exports = {
  reactStrictMode: true,
  experimental: {
    appDir: true,
    swcPlugins: [
      [ 'next-superjson-plugin', {} ]
    ]
  },
  async redirects() {
    return [
      {
        source: '/',
        destination: '/documents',
        permanent: true,
      },
    ]
  },
  transpilePackages: ['ui'],
};

Additional context

File document-list-context.tsx

'use client';

import type { Dispatch, ReactNode } from 'react';
import { createContext, useReducer } from 'react';
import type { DocumentRevisionModel } from 'api-client';

export enum DocumentListContextActionKind {
  SetDocuments = 'setDocuments',
  AddDocument = 'addDocument',
}

export type DocumentListContextAction =
  | {
      type: DocumentListContextActionKind.SetDocuments;
      documents: Array<DocumentRevisionModel>;
    }
  | {
      type: DocumentListContextActionKind.AddDocument;
      document: DocumentRevisionModel;
    };

export interface DocumentListContextState {
  documents: Array<DocumentRevisionModel>;
}

export interface DocumentListContextInterface extends DocumentListContextState {
  documents: Array<DocumentRevisionModel>;
  dispatch: Dispatch<DocumentListContextAction>;
}

export const DocumentListContext = createContext<DocumentListContextInterface>({
  documents: [],
  dispatch: () => {
    throw new Error('DocumentListContext not initialized');
  },
});

interface DocumentListContextProviderProps {
  children: ReactNode;
  initialDocuments?: Array<DocumentRevisionModel>;
}

export function DocumentListContextProvider({
  children,
  initialDocuments = [],
}: DocumentListContextProviderProps) {
  const [state, dispatch] = useReducer(
    (state: DocumentListContextState, action: DocumentListContextAction) => {
      switch (action.type) {
        case DocumentListContextActionKind.SetDocuments:
          return {
            ...state,
            documents: action.documents,
          };
        case DocumentListContextActionKind.AddDocument:
          return {
            ...state,
            documents: [...state.documents, action.document],
          };
        default:
          return state;
      }
    },
    {
      documents: initialDocuments,
    },
  );

  return (
    <DocumentListContext.Provider
      value={{
        documents: state.documents,
        dispatch,
      }}
    >
      {children}
    </DocumentListContext.Provider>
  );
}

File page.tsx


export default async function Page() {
  const apiConfiguration = createConfiguration({
    baseServer: new ServerConfiguration('http://localhost:3001', {}),
  });
  const documentsApi = new DocumentsApi(apiConfiguration);
  const documents = await documentsApi.documentsControllerList();

  return (
    <DocumentListContextProvider
      initialDocuments={documents.documents.map((document) => ({
        ...document,
      }))}
      data-superjson
    >
      {/* both will warn on this layer*/}
      <div />
      <div>
        <div>this div will not warn because there is only one child</div>
      </div>
    </DocumentListContextProvider>
  );
}
AmazingTurtle commented 1 year ago

Im on next 13.5.3 and next-superjson-plugin 0.5.9, bug is still alive.

aplr commented 7 months ago

For me it's happening even with just 1 child. Notice, that ProductDetails is a server component, while ProductProvider (ofc) is not.

<ProductProvider value={state} data-superjson>
    <ProductDetails />
</ProductProvider>

observed on next@14.1.1-canary.49, next-superjson-plugin@0.6.2

AmazingTurtle commented 7 months ago

A workaround that worked for me at the moment is wrapping the values passed in a "wrapper" using the class-transformer.

import { instanceToPlain } from 'class-transformer';

export function superjsonWrapper<T extends object>(obj: T): T {
  return instanceToPlain(obj) as T;
}

That seems to work for me!

grmlin commented 6 months ago

experience the exact same issue as described and have no idea how to fix it. For now, I don't use the plugin and simply ignore the warnings about dates passed around between server and client components. 🤷🏻

The solution proposed by @AmazingTurtle sadly didn't work for me.