loading CKEditor5 components with React.lazy - reactjs

In my project I want to use CKEditor5. Problem is, this version is not compatible with IE11 so my goal is to lazy load CKEditor5 components and when IE11 is detected, I dont want to simply load those components.
When component is imported like this import CKEditor from "#ckeditor/ckeditor5-react"; it just importing class, however with React.lazy import component is wrapped with React.LazyExoticComponent.
I need to create instance of GFMDataProcessor according to documentation https://ckeditor.com/docs/ckeditor5/latest/features/markdown.html
But with dynamic import I am not able to do that, since this line of code throws an error and I dont know what argument should I pass, since GFMDataProcessor is React.LazyExoticComponent and not GFMDataProcessor class.
//Expected 1 arguments, but got 0
const markdownPlugin = (editor) => { editor.data.processor = new GFMDataProcessor() }
Other problem is with my configuration for CKEditor, it has to be lazy loaded also and here is the same problem as above, ClassicEditor is again React.LazyExoticComponent instead of class and I have to pass to editor property imported component, not the wrapped one with React.LazyExoticComponent. Is there some way how I can extract dynamically imported component from wrapped one or any other way how can this be solved?
const ckeditorProps = {
data: data,
editor: ClassicEditor,
onChange: (event, editor) => {
const data2 = editor.getData();
if (data2 !== data) {
this.onChange(data2, this.state.selectedCultureCode, true);
}
},
config: editorConfiguration
}
Here are my dynamic imports
const CKEditor = React.lazy(() => import("#ckeditor/ckeditor5-react"));
const ClassicEditor = React.lazy(() => import("#ckeditor/ckeditor5-build-classic"));
const GFMDataProcessor = React.lazy(() => import("#ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor"));
Usage in DOM structure
<React.Suspense fallback={<div>Loading...</div>}>
<CKEditor {...ckeditorProps} />
</React.Suspense>

I don't know why you wrap anything you want to be fetched asynchronously into the React.lazy components. You should just fetch them normally when they're needed. Maybe something like the following will work for you:
let ClassicEditor, GFMDataProcessor;
const LazyCKEditor = React.lazy(() => {
return Promise.all([
import("#ckeditor/ckeditor5-build-classic"),
import("#ckeditor/ckeditor5-react"),
import("#ckeditor/ckeditor5-markdown-gfm/src/gfmdataprocessor")
]).then(([ _ClassicEditor, _CKEditor, _GFMDataProcessor ]) => {
ClassicEditor = _ClassicEditor;
GFMDataProcessor = _GFMDataProcessor;
return _CKEditor;
});
});

Related

Why does useMediaQuery always return false in React.js

Recently I had a problem with useMediaQuery from "react-responsive".
import { useMediaQuery } from 'react-responsive'
const TestComponent = () => {
const isDesktopOrLaptop = useMediaQuery({
query: '(min-width: 1440px)'
})
const isTablet = useMediaQuery({ query: '(min-width: 768px)' })
const isMobile = useMediaQuery({ query: '(max-width: 475px)' })
}
But I correctly remember the time I used this module successfully last year but at the moment this returns always false.
I don't figure out what is happening with it.
Thanks for your all advice.
I'm unfamiliar with react-responsive, but if you're not attached to using that library, MUI provides the same hook and it works perfectly.
If you're interested in doing it yourself, I've also included the code for a custom hook that works just as well (but has much less functionality).
All-in-one external library:
useMediaQuery from MUI. Below is a simple example that is watching for 600px screen size.
import * as React from 'react'
import useMediaQuery from '#mui/material/useMediaQuery'
export default function SimpleMediaQuery()
{
const matches = useMediaQuery('(min-width:600px)')
return <span>{`(min-width:600px) matches: ${matches}`}</span>
}
Want to use the preset breakpoints instead?
const matches = useMediaQuery(theme.breakpoints.up('sm'))
Callback?
const matches = useMediaQuery((theme) => theme.breakpoints.up('sm'))
This hook can be customized in multiple ways (see the docs linked above for more examples).
Custom hook with no external libraries:
If you want to do it without relying on an external library, here is a simple hook that watches for certain breakpoints, similar to MUI's useMediaQuery.
export const useMediaQuery = (width) =>
{
const [targetReached, setTargetReached] = useState(false)
const updateTarget = useCallback((e) =>
{
if (e.matches) setTargetReached(true)
else setTargetReached(false)
}, [])
useEffect(() =>
{
const media = window.matchMedia(`(max-width: ${width}px)`)
media.addEventListener('change', updateTarget)
// Check on mount (callback is not called until a change occurs)
if (media.matches) setTargetReached(true)
return () => media.removeEventListener('change', updateTarget)
}, [])
return targetReached
}
Usage:
// 600px
const matches = useMediaQuery(600)
I hope you check your current react version firstly, if you are using old version, please update the latest version. I am using react 18.0, it is working on my side perfectly.
Hope it is helpful to you.

React lazy import can't import the component if use string varibale

In my project I have a component render the child node according to the current URL, it uses the React.lazy to import the children components. I implement it followed this post answer's code example.
I noticed a strange thing when I implement it, if I import the component using template strings like the following code shows, it works fine.
const Lzy = React.lazy(() => import(`${currentConfig.componentPath}`));
But when I import the components using the variable directly, it always has an error message on the web pages.
const Lzy = React.lazy(() => import(currentConfig.componentPath));
Error Message:
Error: Cannot find module './home/order/OrderForm'
(anonymous function)
src/features lazy groupOptions: {} namespace object:5
The array of currentConfig object
const pathConfig = [{path: "/dashboard/order/new", componentPath: './home/order/OrderForm'}]
Code snippet
const [CurrentChild, setCurrentChild] = useState(() => () => <div>loading...</div>)
useEffect(() => {
let currentConfig = pathConfig.find(item => item.path === currentPath)
if (!!currentConfig) {
const Lzy = React.lazy(() => import(currentConfig.componentPath));
//const Lzy = React.lazy(() => import(`${currentConfig.componentPath}`));
setCurrentChild(Lzy)
}
}, [currentPath])
Why the first one can work but the second has the can't find the module error, any difference between these two lines code, anyone can help on this?
You can't use in that way, You can refer ECMA Doc https://262.ecma-international.org/6.0/#sec-imports
ImportDeclaration :
import ImportClause FromClause ;
import ModuleSpecifier ;
FromClause :
from ModuleSpecifier
ModuleSpecifier :
from ModuleSpecifier
A ModuleSpecifier can only be a StringLiteral, not any other kind of expression like an AdditiveExpression.

Using addDecorator in Storybook's preview.ts throws Rendered more hooks than during the previous render

Reading through the resource loading documentation from Chromatic, the Solution B: Check fonts have loaded in a decorator section.
Mainly would like to load our fonts before rendering the stories. The solution suggest to use addDecorator where with a simple FC we can preload the fonts and once they are loaded it can render the stories with story().
See the suggested decorator for preview.ts:
import isChromatic from "chromatic/isChromatic";
if (isChromatic() && document.fonts) {
addDecorator((story) => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true);
useEffect(() => {
Promise.all([document.fonts.load("1em Roboto")]).then(() =>
setIsLoadingFonts(false)
);
}, []);
return isLoadingFonts ? null : story();
});
}
For some reason this throws the usual error when violating the Rules of Hooks:
Rendered more hooks than during the previous render
What I've tried so far:
Mainly I tried to remove the useEffect which renders the stories:
if (isChromatic() && document.fonts) {
addDecorator((story) => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true);
return story();
});
}
Also the error disappeared but the fonts are causing inconsistent changes our screenshot tests as before.
Question:
I don't really see any issues which would violate the Rules of Hooks in the added FC for the addDecorator.
Is there anything what can make this error disappear? I'm open to any suggestions. Maybe I missed something here, thank you!
Mainly what solved the issue on our end is removing from main.ts one of the addons called #storybook/addon-knobs.
Also renamed from preview.ts to preview.tsx and used the decorator a bit differently as the following:
export const decorators = [
Story => {
const [isLoadingFonts, setIsLoadingFonts] = useState(true)
useEffect(() => {
const call = async () => {
await document.fonts.load('1em Roboto')
setIsLoadingFonts(false)
}
call()
}, [])
return isLoadingFonts ? <>Fonts are loading...</> : <Story />
},
]
We dropped using the addDecorator and used as the exported const decorators as above.

How to mock a third party React component using Jest?

TLDR; what's the proper way to mock a React component imported from a third-party library?
I'm testing a component called <App/>. It consumes a 3rd part component called <Localize/> provided by a library called localize-toolkit.
I'm having some trouble mocking <Localize/> using Jest.
Here is how I've tried mocking it.
jest.mock('localize-toolkit', () => ({
// Normally you pass in a key that represents the translated caption.
// For the sake of testing, I just want to return the key.
Localize: () => (key:string) => (<span>{key}</span>)
}));
And I've written a unit test for <App/> that looks like this.
it('Test', () => {
const component = render(<App/>);
expect(component).toMatchSnapshot();
}
)
It will pass, however this is the warning message returned.
Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render.
And when I look at the snapshot, I get a series of periods "..." where the localized caption should appear.
Am I not mocking the Localize component properly?
Here's how I ended up doing it.
Note how the third-party component Localize needs to be returned as a function.
jest.mock('localize-toolkit', () => ({
Localize: ({t}) => (<>{t}</>)
}));
and in case there are multiple components, and you only want to mock one of them, you can do this:
jest.mock("localize-toolkit", () => {
const lib = jest.requireActual("localize-toolkit");
return {
...lib,
Localize: ({t}) => (<>{t}</>),
};
});
We can mock the 3rd party library for example in my case i need to mock react-lazyload
Component.tsx
import LazyLoad from 'react-lazyload';
render() {
<LazyLoad><img/></LazyLoad>
}
In jest.config.js
module.exports = {
moduleNameMapper: {
'react-lazyload': '/jest/__mocks__/react-lazyload.js',
}
}
In jest/mocks/react-lazyload.js
import * as React from 'react';
jest.genMockFromModule('react-lazyload');
const LazyLoad = ({children}) => <>{children}</>;
module.exports = { default: LazyLoad };

Dynamically importing files into React component

I'm using create-react-app and a couple external modules, like react-markdown. How do I 'require' a dynamically generated filename? I have thousands of markdown files and I need to load them on the fly. My component works fine if I supply a static path, but doesn't work when I try to concatenate the path. I can't import statically because there are too many files.
Example:
import React, { useState, useEffect } from "react";
import ReactMarkdown from "react-markdown/with-html"
const MarkdownServer = ({myMarkdownFilename}) => {
const [markdown, setValue] = useState([]);
useEffect(() => {
async function fetchData() {
// this works...
// const p = require("../../assets/markdown/mymarkdown.md")
// this does not.
const p = require("../../assets/markdown/" + myMarkdownFilename);
const markdown = await fetch(p).then(res => res.text());
setValue(markdown);
}
fetchData();
}, []);
return (
<p>
<ReactMarkdown source={ markdown } escapeHtml={false} />
</p>
);
};
export default MarkdownServer;
Error:
Unhandled Rejection (Error): Cannot find module '../../assets/markdown/mymarkdown.md'
Have you tried using React's lazy and suspense API's for dynamic importing?
I haven't gotten to use it myself yet but it seems like you could replace the require() with something like const p = React.lazy(() => import("../../assets/markdown/" + myMarkdownFilename)); and then have the return statement render the ReactMarkdown component inside of a Suspense component.
If you're using webpack you might also want to look here:
Let me know how it goes!
Solution is to create a context module with the Webpack context module API. With a bit of regex, I was able to require the markdown docs.
After my standard imports:
const cache = {};
function importAll (r) {
r.keys().forEach(key => cache[key] = r(key));
}
importAll(require.context("../../assets/markdown/", true, /\.md$/));
The cache object now contains the required files and their hashed names as name:value pairs. In my code I simply build the filename and use it to access the property:
useEffect(() => {
async function fetchData() {
filepath = "../../assets/markdown/" + myMarkdownFilename
const p = cache[filepath]
const markdown = await fetch(p).then(res => res.text());
setValue(markdown);
}
fetchData();
}, []);
This being said, there really ought to be a saner way to import static assets.

Resources