Can you deconstruct lazily loaded React components? - reactjs

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 });
}),
);

Related

Warning: lazy: Expected the result of a dynamic import() call when your code is as in documentation

The question and answer is based on https://github.com/parcel-bundler/parcel/issues/2193 but I think it will be helpful to have it on StackOverflow too.
If you coded something similar to the code from React.lazy docs:
import React, {lazy, Suspense} from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
);
}
you may get the following error:
Warning: lazy: Expected the result of a dynamic import() call. Instead
received: function OtherComponent() {}
Your code should look like: const MyComponent = lazy(() => import('./MyComponent'))
But hold on! Your code already looks like const MyComponent = lazy(() => import('./MyComponent'))!
The question and answer is based on https://github.com/parcel-bundler/parcel/issues/2193 but I think it will be helpful to have it on StackOverflow too.
The answer from the article:
A quick solution is to wrap native import, which gets transformed by Parcel, in your own Promise which would return Harmony module-like result, like so:
const OtherComponent = lazy(() => new Promise((resolve, reject) => {
import('./OtherComponent')
.then(result => resolve(result.default ? result : { default: result }))
.catch(reject);
}));
Solution for classes:
Your classes should be written with keyword default:
export default class Foo extends Component {}
Solution for functional components
They should be exported as default:
const MovieDetails = () => {}
export default MovieDetails;

NextJS issue with server side rendering with react-d3-tree

To start off, I have looked at issue number 40 and 95 on the react-d3-tree github
I don't see anything on StackOverflow that would help me. I am trying to implement the parseJSON method so I can take my own JSON file from the project folder and then generate a tree diagram.
Let's start from what I did in the beginning. I copy pasted the example code which worked for 2 seconds before crashing. Reason? Server Side Rendering. Great, so then I find this from NextJS which allows me to disable SSR for some components. Hey, now the example code is working. Let's try the example code where they use external data! Nope, it can't find the parseJSON method. I have no idea what to do, can't find anything to fix this. I am trying to import this function that has some issue with SSR, but because it isn't a component I am not able to import it using dynamic, and I can't import normally because it causes a "window is not defined" error because of SSR.
The following are my main two files.
DynamicComponent.js [Version 1]
import dynamic from 'next/dynamic';
const Tree = dynamic(
() => import('react-d3-tree'),
{ ssr: false },
);
export default Tree;
DynamicComponent.js [Version 2]
import dynamic from 'next/dynamic';
export const Tree = dynamic(
() => import('react-d3-tree'),
{ ssr: false },
);
export const treeUtil = dynamic(
() => import('react-d3-tree/src/util'),
{ ssr: false },
);
Diagram/index.js
import React from 'react';
import { Tree, treeUtil } from '../DynamicComponent';
const myTreeData = require('../fakeData.json');
class Diagram extends React.PureComponent {
constructor() {
super();
this.state = {
data: undefined,
};
}
componentWillMount() {
treeUtil.parseJSON(myTreeData)
.then((data) => {
this.setState({ data });
});
}
render() {
return (
<div
id="treeWrapper"
style={{ width: '50em', height: '20em' }}
>
<Tree data={this.state.data} />
</div>
);
}
}
export default Diagram;
Error I Get with Version 1
ReferenceError: treeUtil is not defined
Error I Get with Version 2
TypeError: _DynamicComponent__WEBPACK_IMPORTED_MODULE_1__.treeUtil.parseJSON is not a function
Please StackOverflow, you're my only hope.
I ran into the same problem with Cytoscape, a similar library (but specifically for graph-network visualization). It took lots of trial and error, but the solution was:
import the component dynamically
remove the import JSON and inline it into a js file. For some stupid reason, that worked for me and was the magic fix. (how big was your JSON file?) Worse-case try copying & pasting into the component itself.
For your component try this:
// convert fakeData.json to fakeData.js
export default {...fake data here };
import React from 'react';
import dynamic from 'next/dynamic'
import myTreeData from 'fakeData';
const Tree = dynamic(
() => import('./dynamicComponent'),
{ ssr: false }
);
// you can also delineate a loading component;
// converted to hooks for '21
const Diagram = () => {
const [data,setData] = useState({});
useEffect(() => {
treeUtil.parseJSON(myTreeData)
.then((data) => {
setData(data);
})
},[treeUtil,myTreeData,setData]);
return (
<div
id="treeWrapper"
style={{ width: '50em', height: '20em' }}
>
<Tree data={data} />
</div>
);
}
export default Diagram;
I guess treeUtil is not a react component, so you can't use dynamic to import it. Just import it normally may be work.
import dynamic from 'next/dynamic';
export const Tree = dynamic(
() => import('react-d3-tree'),
{ ssr: false },
);
export { default as treeUtil } from 'react-d3-tree/src/util';

How to use portals in Next.js in getting the child element outside the immediate parent container?

Current Implementation
Container
export const FormContainer = () => {
return (<Form/>);
}
Component
export const Form = () => {
return ReactDOM.createPortal(
<aside>
<div>{"I am a component"}<div/>
</aside>,
document.body
);
}
Errors
ReferenceError: document is not defined
Expectation
I want the Form to be out of FormContainer DOM hierarchy.
ReferenceError: document is not defined
ReactDOM.createPortal is more like a "BrowserSide only" component because it needs document and in serverSide Rendering, it's undefined.
I want the Form to be out of FormContainer DOM hierarchy.
easiest solution:
set a condition to check for BrowserOnly rendering of createPortal component :
according to saimeunt answer, something like
export const FormContainer = () => {
return (
{process.browser?
<Form/>:null
}
);
}
will make the day.
another way around will be using nextJs dynamic import and disable SSR on module
import dynamic from "next/dynamic";
const Form = dynamic(() => import("path/to/FormModule"), { ssr: false });
export const FormContainer = () => {
return (<Form/>);
}

Using React.lazy with TypeScript

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

React component test keeps failing

I keep getting this error when testing my TodoList component:
debug.html:38
Invariant Violation:
Element type is invalid:
expected a string (for built-in components) or a class/function (for composite components) but got: object.
I'm not sure what's causing it. Could anyone lead me into the right direction?
TodoList.js:
import React from 'react';
import Todo from 'Todo';
class TodoList extends React.Component {
renderTodos(todos) {
return todos.map(todo => {
return <Todo key={todo.id} {...todo} />;
});
}
render() {
const { todos } = this.props;
return (
<div>
{this.renderTodos(todos)}
</div>
);
}
}
export default TodoList;
TodoList.test.js:
const React = require('react');
const ReactDOM = require('react-dom');
const TestUtils = require('react-addons-test-utils');
const expect = require('expect');
const $ = require('jQuery');
const TodoList = require('TodoList');
const Todo = require('Todo');
describe('TodoList', () => {
it('should exist', () => {
expect(TodoList).toExist();
});
it('should render one Todo component for each todo item', () => {
const todos = [{
id: 1,
text: "Do something"
}, {
id: 2,
text: "Check mail"
}];
const todoList = TestUtils.renderIntoDocument(<TodoList todos={todos} />);
const todoComponents = TestUtils.scryRenderedComponentsWithType(todoList, Todo);
expect(todoComponents.length).toBe(todos.length);
});
});
Link to source code: link to source code
In your TodoList.Test.js, remove
const TodoList = require('TodoList');
const Todo = require('Todo');
and replace it with
import TodoList from 'TodoList'
import Todo from 'Todo
That is because you are exporting your components in the ES6 way and importing it the nodejs way.
If you still want to use require in your test then you will have to replace the
export default TodoList
with
module.exports = TodoList;
Hope it helps :)
alternatively:
const TodoList = require('TodoList').default;
const Todo = require('Todo').default;
By default in Node, if you use this syntax:
require('Todo')
It will look for the node module Todo, as in it will look in your node_modules directory for a directory named Todo. Node doesn't know how to use custom Webpack resolves. You have this setting:
modulesDirectories: [
'node_modules',
'./app/components'
],
Meaning any time Webpack sees a require('Todo') or import from 'Todo' it will check both in node_moudles for a file or folder named Todo, and it will check app/components for a folder.
If you want to use the same resolve trick in your tests, you have to build a test bundle with Webpack and run that bundle, the same way you build a browser bundle.
Otherwise you have to use the default Node way to require files not in node_modules, which is with relative paths:
const TodoList = require('../../components/TodoList');
const Todo = require('../../components/Todo');

Resources