Using React.lazy with TypeScript - reactjs

I am trying to use React.lazy for code splitting in my TypeScript React app.
All I am doing is changing that line:
import {ScreensProductList} from "./screens/Products/List";
to this line:
const ScreensProductList = lazy(() => import('./screens/Products/List'));
But the import('./screens/Products/List') part triggers a TypeScript error, stating:
Type error: Type 'Promise<typeof import("/Users/johannesklauss/Documents/Development/ay-coding-challenge/src/screens/Products/List")>' is not assignable to type 'Promise<{ default: ComponentType<any>; }>'.
Property 'default' is missing in type 'typeof import("/Users/johannesklauss/Documents/Development/ay-coding-challenge/src/screens/Products/List")' but required in type '{ default: ComponentType<any>; }'.
I am not quite sure what I am supposed to do here to get it to work.

You should do export default class {...} from the ./screens/Products/list instead of export class ScreensProductList {...}.
Or, alternatively, you can do:
const ScreensProductList = lazy(() =>
import('./screens/Products/List')
.then(({ ScreensProductList }) => ({ default: ScreensProductList })),
);

One option is to add default export in "./screens/Products/List" like that
export default ScreensProductList;
Second is to change import code to
const ScreensProductList = React.lazy(() =>
import("./screens/Products/List").then((module) => ({
default: module.ScreensProductList,
}))
);
Or if you don't mind using an external library you could do:
import { lazily } from 'react-lazily';
const { ScreensProductList } = lazily(() => import('./screens/Products/List'));

Another solution would be:
1. Import using lazy
const ScreensProductList = lazy(() => import('./screens/Products/List'));
2. Set the type on the export
React hooks
import { FunctionComponent /*, FC */ } from 'react';
const List = () => (
return </>;
);
export default List as FunctionComponent; // as FC;
React class components
import { Component, Fragment, ComponentType } from 'react';
class List extends Component {
render() {
return <Fragment />;
}
}
export default List as ComponentType;

This is the proper syntax. It works also in the Webstorm IDE (the other syntaxes shown here are still showing a warning)
const ScreensProductList = React.lazy(() => import("./screens/Products/List").then(({default : ScreensProductList}) => ({default: ScreensProductList})));

const LazyCart = React.lazy(async () => ({ default: (await import('../Components/market/LazyCart')).LazyCart }))

You can create an index.ts file where you can export all your components like in this eg. :
export {default as YourComponentName} from "./YourComponentName";
After that you can use React.lazy:
React.lazy(() => import("../components/folder-name-where-the-index-file-is-created").then(({YourComponentName}) => ({default: YourComponentName})))

Just to expand on this answer. This also works for the dynamic imports.
const Navbar = dynamic(() => import('../components/Navbar'), {
ssr: false,
});
Where Navbar is a default exported component.
const Navbar = () => ()
export default Navbar

Related

Nextjs, react and TypeScript: prop type missing on FC using getInitialProps when used

I have a very basic React and Nextjs project built in TypeScript. I'm currently building this project from scratch to understand Nextjs better, I'm already quite proficient with TypeScript and React.
I have a simple Clock component built like below, and as per the nextjs documentation.
import { NextPage } from 'next';
import React, { useEffect } from 'react';
type Props = {
isoTime: string
}
const Clock: NextPage<Props> = ({ isoTime }: Props) => {
return <p>{isoTime}</p>;
}
Clock.getInitialProps = async () => {
return {
isoTime: new Date().toISOString()
};
};
export default Clock;
No problems here, however when I try and use this component elsewhere, for example in my app component...
import React from 'react';
import Clock from './clock/Clock';
function App() {
return (
<div className="App">
<Clock />
</div>
);
}
export default App;
I get the TypeScript error Property 'isoTime' is missing in type '{}' but required in type 'Props'. If I add a {/*#ts-ignore*/} above the error it compiles and works as expected, however I shouldn't have to remove typesafety just to get this to compile. How can I get TypeScript to pick up the props coming from getInitialProps?
According to nextJS documentation the put it as a optional props
interface Props {
userAgent?: string;
}
const Page: NextPage<Props> = ({ userAgent }) => (
<main>Your user agent: {userAgent}</main>
)
Page.getInitialProps = async ({ req }) => {
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
return { userAgent }
}
export default Page
From you code it will be like this
type Props = {
isoTime?: string
}

Persistent layout in Next.js with TypeScript [duplicate]

I want to add a persistent layout to certain pages of my Next.js application. I found this article explaining a couple ways on how someone could do this. It seems pretty straightforward, however I have encountered the following two problems when using the recommended way of doing it:
I am using TypeScript and am not sure how to type it. For example, I have the following, which is working, but I obviously don't like using as any:
const getLayout =
(Component as any).getLayout ||
((page: NextPage) => <SiteLayout children={page} />);
I am using Apollo and so I am using a withApollo HOC (from here) for certain pages. Using this causes Component.getLayout to always be undefined. I don't have a good enough understanding of what is going on to know why this is happening (I can guess), so it's difficult to solve this by myself.
Since asking this question they have added a good example to their documentation
I have the similar problem and this is how I solved it for my project.
Create a types/page.d.ts type definition:
import { NextPage } from 'next'
import { ComponentType, ReactElement, ReactNode } from 'react'
export type Page<P = {}> = NextPage<P> & {
// You can disable whichever you don't need
getLayout?: (page: ReactElement) => ReactNode
layout?: ComponentType
}
In your _app.tsx file,
import type { AppProps } from 'next/app'
import { Fragment } from 'react'
import type { Page } from '../types/page'
// this should give a better typing
type Props = AppProps & {
Component: Page
}
const MyApp = ({ Component, pageProps }: Props) => {
// adjust accordingly if you disabled a layout rendering option
const getLayout = Component.getLayout ?? (page => page)
const Layout = Component.layout ?? Fragment
return (
<Layout>
{getLayout(<Component {...pageProps} />)}
</Layout>
)
// or swap the layout rendering priority
// return getLayout(<Layout><Component {...pageProps} /></Layout>)
}
export default MyApp
The above is just a sample implementation best suited for my use-case, you can switch the type in types/page.d.ts to fit your needs.
You can extend Next type definitions by adding getLayout property to AppProps.Component.
next.d.ts (as of Next 11.1.0):
import type { CompletePrivateRouteInfo } from 'next/dist/shared/lib/router/router';
import type { Router } from 'next/dist/client/router';
declare module 'next/app' {
export declare type AppProps = Pick<CompletePrivateRouteInfo, 'Component' | 'err'> & {
router: Router;
} & Record<string, any> & {
Component: {
getLayout?: (page: JSX.Element) => JSX.Element;
}
}
}
pages/_app.tsx:
import type { AppProps } from 'next/app';
const NextApp = ({ Component, pageProps }: AppProps) => {
// Typescript does not complain about unknown property
const getLayout = Component.getLayout || ((page) => page);
// snip
}
If you want to have even more type-safety, you can define custom NextPageWithLayout type which will assert that your component has getLayout property set.
next.d.ts (as of Next 11.1.0):
import type { NextPage } from 'next';
import type { NextComponentType } from 'next/dist/next-server/lib/utils';
declare module 'next' {
export declare type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout: (component: NextComponentType) => JSX.Element;
};
}
pages/example-page/index.tsx:
import type { NextPageWithLayout } from 'next';
const ExamplePage: NextPageWithLayout<ExamplePageProps> = () => {
// snip
}
// If you won't define `getLayout` property on this component, TypeScript will complain
ExamplePage.getLayout = (page) => {
// snip
}
Note that while extending Next type definitions you have to provide exact type/interface signature (together with matching generics), otherwise declaration merging won't work. This unfortunately means adjusting type definitions if library changes API.
I believe next.js provides a Component prop as a part of AppProps. This example should have what you are looking for.
You may need to assign getLayout after creating the component wrapped with withApollo:
import CustomLayout = CustomLayout;
const MyPage = () => (...);
const MyPageWithApollo = withApollo(MyPage);
MyPageWithApollo.Layout = CustomLayout;
export default MyPageWithApollo;
Additionally, next.js has two examples you can use to achieve exactly this (not written in typescript though):
Dynamic App Layout
With Apollo
// if no Layout is defined in page, use this
const Default: FC = ({children}) => <>{children}</>
function MyApp({Component, pageProps}: AppProps & {Component: {Layout: FC}}) {
// this will make layput flexible.
// Layout is extracted from the page
const Layout = Component.Layout ?? Default
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
export default MyApp
Let's say you want to use the Layout in "Home",
import { Layout } from "#components"
export default function Home({
...
}
Home.Layout = Layout

Persistent layout in Next.js with TypeScript and HOC

I want to add a persistent layout to certain pages of my Next.js application. I found this article explaining a couple ways on how someone could do this. It seems pretty straightforward, however I have encountered the following two problems when using the recommended way of doing it:
I am using TypeScript and am not sure how to type it. For example, I have the following, which is working, but I obviously don't like using as any:
const getLayout =
(Component as any).getLayout ||
((page: NextPage) => <SiteLayout children={page} />);
I am using Apollo and so I am using a withApollo HOC (from here) for certain pages. Using this causes Component.getLayout to always be undefined. I don't have a good enough understanding of what is going on to know why this is happening (I can guess), so it's difficult to solve this by myself.
Since asking this question they have added a good example to their documentation
I have the similar problem and this is how I solved it for my project.
Create a types/page.d.ts type definition:
import { NextPage } from 'next'
import { ComponentType, ReactElement, ReactNode } from 'react'
export type Page<P = {}> = NextPage<P> & {
// You can disable whichever you don't need
getLayout?: (page: ReactElement) => ReactNode
layout?: ComponentType
}
In your _app.tsx file,
import type { AppProps } from 'next/app'
import { Fragment } from 'react'
import type { Page } from '../types/page'
// this should give a better typing
type Props = AppProps & {
Component: Page
}
const MyApp = ({ Component, pageProps }: Props) => {
// adjust accordingly if you disabled a layout rendering option
const getLayout = Component.getLayout ?? (page => page)
const Layout = Component.layout ?? Fragment
return (
<Layout>
{getLayout(<Component {...pageProps} />)}
</Layout>
)
// or swap the layout rendering priority
// return getLayout(<Layout><Component {...pageProps} /></Layout>)
}
export default MyApp
The above is just a sample implementation best suited for my use-case, you can switch the type in types/page.d.ts to fit your needs.
You can extend Next type definitions by adding getLayout property to AppProps.Component.
next.d.ts (as of Next 11.1.0):
import type { CompletePrivateRouteInfo } from 'next/dist/shared/lib/router/router';
import type { Router } from 'next/dist/client/router';
declare module 'next/app' {
export declare type AppProps = Pick<CompletePrivateRouteInfo, 'Component' | 'err'> & {
router: Router;
} & Record<string, any> & {
Component: {
getLayout?: (page: JSX.Element) => JSX.Element;
}
}
}
pages/_app.tsx:
import type { AppProps } from 'next/app';
const NextApp = ({ Component, pageProps }: AppProps) => {
// Typescript does not complain about unknown property
const getLayout = Component.getLayout || ((page) => page);
// snip
}
If you want to have even more type-safety, you can define custom NextPageWithLayout type which will assert that your component has getLayout property set.
next.d.ts (as of Next 11.1.0):
import type { NextPage } from 'next';
import type { NextComponentType } from 'next/dist/next-server/lib/utils';
declare module 'next' {
export declare type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout: (component: NextComponentType) => JSX.Element;
};
}
pages/example-page/index.tsx:
import type { NextPageWithLayout } from 'next';
const ExamplePage: NextPageWithLayout<ExamplePageProps> = () => {
// snip
}
// If you won't define `getLayout` property on this component, TypeScript will complain
ExamplePage.getLayout = (page) => {
// snip
}
Note that while extending Next type definitions you have to provide exact type/interface signature (together with matching generics), otherwise declaration merging won't work. This unfortunately means adjusting type definitions if library changes API.
I believe next.js provides a Component prop as a part of AppProps. This example should have what you are looking for.
You may need to assign getLayout after creating the component wrapped with withApollo:
import CustomLayout = CustomLayout;
const MyPage = () => (...);
const MyPageWithApollo = withApollo(MyPage);
MyPageWithApollo.Layout = CustomLayout;
export default MyPageWithApollo;
Additionally, next.js has two examples you can use to achieve exactly this (not written in typescript though):
Dynamic App Layout
With Apollo
// if no Layout is defined in page, use this
const Default: FC = ({children}) => <>{children}</>
function MyApp({Component, pageProps}: AppProps & {Component: {Layout: FC}}) {
// this will make layput flexible.
// Layout is extracted from the page
const Layout = Component.Layout ?? Default
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
export default MyApp
Let's say you want to use the Layout in "Home",
import { Layout } from "#components"
export default function Home({
...
}
Home.Layout = Layout

Using switch statement with dynamic components in NextJS

I am trying to use dynamic import in NextJS and I do not understand why it works only while storing imported component in a variable. It breaks when I try to return it from other function.
It works this way:
import dynamic from "next/dynamic";
const Article = dynamic(() => import("tutorial/ru/welcome.mdx"));
but like this, well, it breaks:
import dynamic from "next/dynamic";
export default ({ route }) => {
switch (route) {
case "ru":
default:
return dynamic(() => import("tutorial/ru/welcome.mdx"));
}
};
I get the Invalid hook call. Hooks can only be called inside of the body of a function component message.
I think you need to export it , then try to use it like so :
import dynamic from "next/dynamic";
const Article = dynamic(() => import("tutorial/ru/welcome.mdx"));
export default Article;
then try to use it in switch statement :
import Article from './Article';
export default ({ route }) => {
switch (route) {
case "ru":
return (<></>)
default:
return <Article />;
}
};
I found a solution to get over this issue!
import dynamic from "next/dynamic";
import Loader from "components/Loader/Loader";
import Error from "pages/_error";
export default ({ route }) => {
const Article = dynamic(
() => import(`tutorial/${route}.mdx`).catch(err => {
return () => <Error />
}),
{ loading: () => <Loader /> }
);
return <Article />
};
I should store the component in the variable after all, but I get the component itself dynamically using literal strings, and after that I return the component as tag (). Works fine now!

Can you deconstruct lazily loaded React components?

Using es6 imports, you can do this:
import { MyComponent } from "../path/to/components.js";
export default function () {
return <MyComponent/>;
}
Can I do it with React.lazy too?
const { MyComponent } = lazy(() => import("../path/to/components.js"));
I get the following error, but I'm not sure if it's related to this or some other bug I have:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined
Here is how I did it when I faced this problem with FontAwesome:
const FontAwesomeIcon = React.lazy(()=> import('#fortawesome/react-fontawesome').then(module=>({default:module.FontAwesomeIcon})))
You can if you use react-lazily.
import { lazily } from 'react-lazily';
const { MyComponent } = lazily(() => import("../path/to/components.js"));
It also allows importing more than one component:
const { MyComponent, MyOtherComponent, SomeOtherComponent } = lazily(
() => import("../path/to/components.js")
);
See this answer for more options.
React.lazy currently only supports default exports. If the module you want to import uses named exports, you can create an intermediate module that reexports it as the default. This ensures that tree shaking keeps working and that you don’t pull in unused components.
// ManyComponents.js
export const MyComponent = /* ... */;
export const MyUnusedComponent = /* ... */;
// MyComponent.js
export { MyComponent as default } from "./ManyComponents.js";
// MyApp.js
import React, { lazy } from 'react';
const MyComponent = lazy(() => import("./MyComponent.js"));
More info:
https://reactjs.org/docs/code-splitting.html#named-exports
Of course you can. It's an honest mistake many has made,
const sub = a
const obj = { a: 'alpha', b: 'beta' }
obj.sub // wrong (accessing a direct key)
obj[sub] // right (computed property)
the same mistake slipped through for many. This is a work in progress but worked like a charm, and thanks for all the other answers to tailor it to my need.
const ComponentFactory = ({ componentName, ...props }) => {
const Component = lazy(() => import('baseui/typography').then((module) => ({ default: module[componentName] })))
return (
<Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</Suspense>
)
}
usage:
<ComponentFactory
componentName='Paragraph1'
margin='0.1rem 0rem 0.25rem 0.3rem'
color={style[of].headingText}
>
{headingMessage}
</ComponentFactory>
You can't with React.lazy :
React.lazy takes a function that must call a dynamic import(). This must return a Promise which resolves to a module with a default export containing a React component.
(cf. https://reactjs.org/docs/code-splitting.html#reactlazy)
A workaround for that exists: creating an intermediate module that imports your named export and exports it as default (cf. https://reactjs.org/docs/code-splitting.html#named-exports)
I'd like to another workaround. This compotent chains the promise and adds the named export to the default export. src. Although, I'm not sure if this breaks tree shaking. There's a bit of an explanation here.
import {lazy} from 'react'
export default (resolver, name = 'default') => {
return lazy(async () => {
const resolved = await resolver()
return {default: resolved[name]}
})
}
You can resolve a promise along with the lazy loading and this way resolve your named export.
The syntax is a bit funky, but it is working:
const MyComponent = React.lazy(
() =>
new Promise(async (resolve) => {
const module = await import('../path/to/components.js');
resolve({ ...module, default: module.default });
}),
);

Resources