Frankey
Frankey

Reputation: 336

Creating a Universal Component in React for Next.js Compatibility

I have 2 projects.

Project A is written with React but does not use the Next.js framework. It is intended to be served as a headless UI library, allowing any project that imports it to access its components.

Project B is written with React and utilizes the Next.js framework. This project serves as the website's frontend and is the primary focus.

I am attempting to create a link component in Project A that behaves like next/link when imported into Project B.

I have tried solution provided here but but it did not work as expected.

I understand that in Next.js, it's crucial to consider both client-side and server-side rendering. I am seeking an approach that can accommodate both scenarios.

The codes I have tried is as below (from Project A - The headless ui project):

const Link = (props: LinkProps) => {
  try {
    return createElement(
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      require.resolve("next/link").default,
      props,
      props.children
    );
  } catch (e) {
    console.log("error getting nextLinkPath or create element", e);
    return createElement("a", props, props.children);
  }
};

export default Link;

When implementing this code into Project B (which uses Next js framework), I encountered an error stating

ReferenceError: require is not defined,

when used in a client side component. When used in a server side component, it shows that require.resolve("next/link").default is undefined.

I have also tried using import

const Link = (props: LinkProps) => {
  loadNextComponent(props).then((component) => {
    return component;
  });
};

const loadNextComponent = async (props: LinkProps) => {
  try {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    //@ts-ignore
    const NextLink = (await (import(/* @vite-ignore */ "next/link") as any)
      .default) as string;
    return createElement(NextLink, { ...props }, props.children);
  } catch (error) {
    console.error("Error loading next component:", error);
    return <a {...props}>{props.children}</a>;
  }
};

The implementation did not work as intended, and it became worse because I couldn't catch errors caused by the import statement during Project A runtime. I have received error stating

[vite] Internal server error: Failed to resolve import "next/link" from "src/components/atoms/Link/Link.tsx". Does the file exist?

I also received this error when implemented into project B

Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

In summary, how can I dynamically call next/link component in a react library that does not use nextjs framework, so that it will served as next/link when imported into any project where next/link is available?

Upvotes: 0

Views: 168

Answers (1)

Eugene Dilbarov
Eugene Dilbarov

Reputation: 1

Same requirments, this works for me:

import { Link as ReactRouterLink } from 'react-router-dom';
import dynamic from 'next/dynamic';

export default function BaseLink({ href, target, className, dataTestId, children }: Props) {
  if (typeof process !== 'undefined') {
    const NextLink = dynamic(() => import('next/link'));
    return (
      <NextLink href={href} className={className} data-testid={dataTestId} target={target}>
        {children}
      </NextLink>
    );
  }

  return (
    <ReactRouterLink to={href} className={className} data-testid={dataTestId} target={target}>
      {children}
    </ReactRouterLink>
  );
}

Upvotes: 0

Related Questions