Exclude react-quill from vendor and bundle size - reactjs

import React from 'react';
import clsx from 'clsx';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
import styles from './styles.scss';
interface Props {
label: string;
value: any;
className?: string;
inputProps: {
onChange: (e: any) => void;
};
}
const RichText = ({ value = '', className, inputProps, label }: Props) => {
const { onChange } = inputProps;
const modules = {
toolbar: [['bold', 'italic', 'underline'], [{ align: [] }]],
};
const formats = ['bold', 'italic', 'underline'];
return (
<div>
<label>{label}</label>
<ReactQuill
value={value}
onChange={onChange}
formats={formats}
modules={modules}
className={clsx(styles.root, className)}
/>
</div>
);
};
export default RichText;
Above you can see my rich-text component , where I user react-quill npm package. I use it only in 1 place in my code , but it add 50-60 kb to my bundle size and it annoying me. I've tried to load it dynamically by doing
const ref = useRef()
useEffect(() => {
import('react-quill').then(data => {
ref.current = data.default
})
}, [])
const ReactQuill = ref.current
But it still sit in my bundle size. I've tried to load it by external url by this hook
import { useState } from 'react'
import { useMountEffect } from 'hooks'
const useExternalLibrary = ({ url, libName }) => {
const [lib, setLib] = useState({})
const fetchJsFromCDN = (src, externals = []) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.setAttribute('src', src)
script.addEventListener('load', () => {
resolve(
externals.map(key => {
return window[key]
})
)
})
script.addEventListener('error', reject)
document.body.appendChild(script)
})
}
useMountEffect(() => {
fetchJsFromCDN(url, [libName]).then(([library]) => {
setLib(library)
})
})
return {
lib
}
}
export default useExternalLibrary
Where you can pass url and how it should be called in global space, url is - https://unpkg.com/react-quill#1.3.3/dist/react-quill.js , but It throw error, that you should have for a start React in global , then it is broken by prop-types library , I don't use it in my project, etc . And I have no idea what else should I try to prevent it be in my bundle size , and load it only when I need it
optimization: {
minimize: true,
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
},
Above you can also see webpack optimization configuration, and also i've tried to wrap it to lazy
const ReactQuill = lazy(() => import('react-quill'));

As per your current webpack configuration, webpack is spitting out all modules inside node_modules as a single chunk named vendor. This is why you are not able to achieve the lazy loading for react-quill.
If you want to create a separate chunk for react-quill, you can modify splitChunks so that it creates a separate chunk for quill and react-quill modules:
splitChunks: {
cacheGroups: {
reactQuillVendor: {
test: /[\\/]node_modules[\\/](quill|react-quill)[\\/]/,
name: 'reactQuillVendor',
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
},
},
},

Related

React Quill - social media link to embed media on rich text editor

I want the react quill rich text editor able to convert link into social media, like this
link: https://www.tiktok.com/#epicgardening/video/7055411162212633903
My RTE Code
import { useCallback, useMemo, useEffect } from 'react';
import ImageResize from 'quill-image-resize-module-react';
import ReactQuill, { Quill } from 'react-quill';
import { message } from 'antd';
import { uploadFiles } from 'utils';
import 'react-quill/dist/quill.bubble.css';
import 'react-quill/dist/quill.snow.css';
import './styles.scss';
Quill.register('modules/imageResize', ImageResize);
const RichTextEditor = ({
editorState,
onChange,
readOnly = false,
setLoading = () => {}
}) => {
window.Quill = Quill;
let quillRef = null; // Quill instance
let reactQuillRef = null; // ReactQuill component
useEffect(() => {
attachQuillRefs();
}, []);
const attachQuillRefs = useCallback(() => {
if (typeof reactQuillRef.getEditor !== 'function') return;
quillRef = reactQuillRef.getEditor();
}, []);
const imageHandler = () => {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'image/*');
input.click();
input.onchange = async () => {
const file = input.files[0];
if (file.size > 1500000)
return message.error('Image size exceeded 1.5Mb');
setLoading({ image: true });
const formData = new FormData();
formData.append('image', file);
const fileName = file.name;
const imgUrl = await uploadFiles(file, quillRef);
const range = quillRef.getSelection();
quillRef.insertEmbed(range.index, 'image', imgUrl, 'user');
let existingDelta = quillRef.getContents();
const indexOf = existingDelta.ops.findIndex((eachOps) => {
return eachOps.insert?.image === imgUrl;
});
const selectedOps = existingDelta.ops[indexOf];
if (indexOf !== -1) {
selectedOps.attributes = {};
selectedOps.attributes = { alt: fileName };
}
quillRef.setContents(existingDelta);
setLoading({ image: false });
};
};
const modules = useMemo(
() => ({
toolbar: {
container: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
['bold', 'italic', 'underline'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ align: [] }],
['link', 'image'],
['clean'],
[{ color: [] }]
],
handlers: {
image: imageHandler
}
},
imageResize: {
modules: ['Resize', 'DisplaySize']
}
}),
[]
);
return (
<div className="react-quill-wrapper">
<ReactQuill
readOnly={readOnly}
theme={readOnly ? 'bubble' : 'snow'}
ref={(e) => {
reactQuillRef = e;
}}
value={editorState}
modules={modules}
placeholder="Add content of your article!"
onChange={onChange}
/>
</div>
);
};
export { RichTextEditor };
const [editorState, setEditorState] = useState('');
<RichTextEditor
editorState={editorState}
onChange={setEditorState}
setLoading={setLoading}
/>
called by parent like this
I've been working on this for almost a week, I really need help
I expected to have an string HTML output, like this library or image above
My attempts:
Get the social media url typed by user based on link, use that typed link to determined what social media, and use react-social-media-embed to give me an output link image above.
I belive(maybe) that the output from react-social-media-embed is jsx, and I need to convert it to html, and parsed it to string.

NextJS turn a .MDX into a component

I am trying to create an MDX blog and integrate it into my project. I currently have most of the setup done but I cannot for the life of me get MDX file into a component to render between my Layout. Here's my issue, all of the examples showcase something weird, for example,
MDX docs aren't typed, and I cannot get this to work in typescript
export default function Page({code}) {
const [mdxModule, setMdxModule] = useState()
const Content = mdxModule ? mdxModule.default : Fragment
useEffect(() => {
;(async () => {
setMdxModule(await run(code, runtime))
})()
}, [code])
return <Content />
}
And Nextjs just advises to put mdx files under /pages/, which doesn't fit my usecase
What I want:
Have an MDX file with Frontmatter YAML metadata
Load that MDX file into a component and display it
What I have currently
Nextjs config
import mdx from '#next/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import remarkGfm from 'remark-gfm';
/** #type {import('next').NextConfig} */
const config = {
eslint: {
dirs: ['src'],
},
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'tsx', 'ts'],
images: {
domains: [
'res.cloudinary.com',
'picsum.photos', //TODO
],
},
// SVGR
webpack: (config, options) => {
config.module.rules.push({
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
use: [
{
loader: '#svgr/webpack',
options: {
typescript: true,
icon: true,
},
},
],
});
config.module.rules.push({
test: /\.mdx?$/,
use: [
options.defaultLoaders.babel,
{
loader: '#mdx-js/loader',
options: {
// providerImportSource: '#mdx-js/react',
remarkPlugins: [remarkFrontmatter, remarkGfm],
},
},
],
});
return config;
},
};
export default config;
My projects/[slug].tsx file
import { GetStaticPaths, GetStaticProps } from 'next';
import { getFileBySlugAndType, getFiles } from '#/lib/mdx/helpers';
import { Layout } from '#/components/layout/Layout';
import Seo from '#/components/Seo';
import * as runtime from 'react/jsx-runtime.js'
import { ProjectFrontMatter } from '#/types/frontmatter';
import { useEffect, useState } from 'react';
import {compile, run} from '#mdx-js/mdx'
import { MDXContent } from 'mdx/types';
type SingleProjectPageProps = {
frontmatter: ProjectFrontMatter;
content: string;
};
export default function SingleProjectPage({
frontmatter,
content,
}: SingleProjectPageProps) {
return (
<Layout>
<Seo
templateTitle={frontmatter.name}
description={frontmatter.description}
date={new Date(frontmatter.publishedAt).toISOString()}
/>
<main>
<section className=''>
<div className='layout'>
<div className='mt-8 flex flex-col items-start gap-4 md:flex-row-reverse md:justify-between'>
{/* <CustomLink
href={`https://github.com/theodorusclarence/theodorusclarence.com/blob/main/src/contents/projects/${frontmatter.slug}.mdx`}
>
Edit this on GitHub
</CustomLink>
<CustomLink href='/projects'>← Back to projects</CustomLink> */}
</div>
</div>
</section>
</main>
</Layout>
);
}
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getFiles('projects');
return {
paths: posts.map((p) => ({
params: {
slug: p.replace(/\.mdx/, ''),
},
})),
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const slug = params!['slug'] as string;
const { data, content } = getFileBySlugAndType(slug, 'projects');
return {
props: { frontmatter: data as ProjectFrontMatter, content },
};
};
And my helper function
export const getFileBySlugAndType = (slug: string, type: ContentType) => {
const file = readFileSync(
join(process.cwd(), 'src', 'content', type, slug + '.mdx'),
'utf-8'
);
const { data, content } = matter(file);
return { data, content };
};
I get as props in my SingleProjectPage, frontmatter data, and a string of the rest of the MDX content, which is correct, but I need to turn this string into MDX component. One of the libraries that does this is MDX-Bundler, but it hasn't been updated this year, and I'd prefer to use mdx-js if possible as it just release 2.0 version.

Storybook: "Element type is invalid" when using an alias

After I made "#" an alias for the src directory, Storybook began to spit the following error for all my components:
Element 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.
Check the render method of unboundStoryFn.
I didn't mix up default and named imports, and I didn't forget to export my components.
All worked perfectly fine before.
Storybook settings:
const path = require("path");
const toPath = (_path) => path.join(process.cwd(), _path);
module.exports = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.#(js|jsx|ts|tsx)"],
addons: [
"#storybook/addon-links",
"#storybook/addon-essentials",
"#storybook/preset-create-react-app",
"#storybook/addon-a11y",
],
framework: "#storybook/react",
webpackFinal: async (config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
// We need this for emotion to work inside storybook
"#emotion/core": toPath("node_modules/#emotion/react"),
"#emotion/styled": toPath("node_modules/#emotion/styled"),
"#": toPath("src"),
},
},
};
},
};
One of the components:
export const Button: React.FC<ButtonProps> = ({
variant = "primary",
...otherProps
}) => {
if (variant === "primary") return <PrimaryButton {...otherProps} />;
return <SecondaryButton {...otherProps} />;
};
Its story:
import { ComponentMeta, ComponentStory } from "#storybook/react";
import React from "react";
import { Button } from "./Button";
export default {
title: "UI/Button",
component: Button,
} as ComponentMeta<typeof Button>;
const Template: ComponentStory<typeof Button> = (args) => {
return <Button {...args}>Button text</Button>;
};
export const Primary = Template.bind({});
export const PrimaryDisabled = Template.bind({});
PrimaryDisabled.args = {
disabled: true,
};
export const Secondary = Template.bind({});
Secondary.args = {
variant: "secondary",
};
export const SecondaryDisabled = Template.bind({});
SecondaryDisabled.args = {
variant: "secondary",
disabled: true,
};
I tried to use default export for the component. It didn't work either.

Custom React Hook Not Working With Rollup

I've created a Typescript React component library and using Rollup to bundle and then publish to npm. Now everything works great in storybook but when I pull the package into another project it just doesn't work. No errors, nothing just the hook doesn't fire.
Here is my rollup.config.js
import commonjs from "#rollup/plugin-commonjs";
import copy from "rollup-plugin-copy";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import postcss from "rollup-plugin-postcss";
import resolve from "#rollup/plugin-node-resolve";
import typescript from "rollup-plugin-typescript2";
const packageJson = require("./package.json");
export default {
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true
},
{
file: packageJson.module,
format: "esm",
sourcemap: true
}
],
external: ['react', 'react-dom'],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ useTsconfigDeclarationDir: true }),
postcss(),
copy({
targets: [
{
src: "src/styles/variables.scss",
dest: "build",
rename: "variables.scss"
},
{
src: "src/styles/typography.scss",
dest: "build",
rename: "typography.scss"
}
]
})
]
};
and here is my hook useResize:
import { useEffect, useState } from 'react';
import styles from '../styles/variables.scss';
export const useResize = (ref) => {
const [size, updateSize] = useState({});
const [sizeIndex, updatesizeIndex] = useState(null);
const xsMax = parseInt(styles.xsMax)
const smMax = parseInt(styles.smMax)
const mdMax = parseInt(styles.mdMax)
const lgMax = parseInt(styles.lgMax)
const xlMax = parseInt(styles.xlMax)
const getDimensions = () => ({
width: ref.current.offsetWidth,
height: ref.current.offsetHeight
});
const setSize = () => {
let dimensions = getDimensions();
if (dimensions.width > xlMax) {
updateSize('xxl');
updatesizeIndex(5);
}
if ((dimensions.width <= xlMax) && (dimensions.width > lgMax)) {
updateSize('xl');
updatesizeIndex(4);
}
if ((dimensions.width <= lgMax) && (dimensions.width > mdMax)) {
updateSize('lg');
updatesizeIndex(3);
}
if ((dimensions.width <= mdMax) && (dimensions.width > smMax)) {
updateSize('md');
updatesizeIndex(2);
}
if ((dimensions.width <= smMax) && (dimensions.width > xsMax)) {
updateSize('sm');
updatesizeIndex(1);
}
if (dimensions.width <= xsMax) {
updateSize('xs');
updatesizeIndex(0);
}
}
useEffect(() => {
const handleResize = () => {
setSize();
}
if (ref.current) {
setSize();
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
}, [ref])
return [size, sizeIndex]
}
And here is how its used:
import "./Row.scss";
import React, { FC, useCallback, useEffect, useRef, useState } from "react";
import { RowProps } from "./Row.types";
import _ from 'lodash';
import clsx from "clsx";
import { useResize } from "./useResize";
const Row: FC<RowProps> = ({ gutter, columns, children, title, subTitle }: RowProps) => {
const rowRef = useRef(null);
const [columnsRef, updateColumnsRef] = useState(columns);
const [size, sizeIndex] = useResize(rowRef);
useEffect(() => {
if (_.isNumber(columns))
return;
!columns[sizeIndex] ?
updateColumnsRef(_.last(columns)) :
updateColumnsRef(columns[sizeIndex]);
console.log(size)
}, [size, sizeIndex]);
const renderStyles = useCallback(() => {
return {
display: 'grid',
gridTemplateColumns: `repeat(${columnsRef}, minmax(0, 1fr))`,
gridGap: gutter,
}
}, [columnsRef])
return <div className={clsx("Row")} ref={rowRef}>
{title &&
<div className="Row-heading">
<h4>{title}</h4>
<h5>{subTitle}</h5>
</div>
}
<div className="Row-container" style={renderStyles()}>
{children}
</div>
</div>
};
Row.defaultProps = {
columns: 12,
gutter: 24
}
export default Row;

Code coverage is not working for some files in WebStorm

I want to write a UT for the target code below:
// dev code
import React, { useState } from 'react'
import { Dropdown } from 'antd'
import './style.scss'
import classnames from 'classnames'
import Menu from '#lib/Menu'
const DEFAULT_TRIGGER_MODE = 'contextMenu'
interface ContextMenuProps {
wrapClass?: string
menu?: { key: string; title: string }[]
children?: object
updateMenu?: (event) => { key: string; title: string }[]
visible?: boolean
triggerMode?: ('click' | 'hover' | 'contextMenu')[]
}
const updateMenuData =(eventObj, updateMenu, setMenu) => {
if (updateMenu) {
const newMenu = updateMenu(eventObj)
setMenu(newMenu)
}
}
const populateMenuNodes = (menuData) => {
if (menuData.length === 0) return <></>
return (
<Menu>
{menuData.map(({ key, title, onClick, disabled }, index) => (
key === 'menu.divider' ? <Menu.Divider key={key} /> : <Menu.Item key={key} onClick={onClick} disabled={disabled}>{title}</Menu.Item>
))}
</Menu>
)
}
const ContextMenu: React.FC<ContextMenuProps> = (props) => {
const [menu, setMenu] = useState(props.menu || [])
const { children, updateMenu, wrapClass, triggerMode = [DEFAULT_TRIGGER_MODE], ...rest } = props
return (
<div
className={classnames('mstr-as-context-menu', wrapClass)}
onContextMenu={(e) => {
updateMenuData(e, updateMenu, setMenu)
}}
>
<Dropdown overlay={populateMenuNodes(menu)} trigger={triggerMode} {...rest}>
{children}
</Dropdown>
</div>
)
}
export default ContextMenu
And I have written a UT for the target code above:
import React from 'react'
import { shallow } from 'enzyme'
import ContextMenu from '../index'
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn((menu) => [menu || []]),
}))
describe('ContextMenu/index.js', () => {
let component
let props
beforeEach(() => {
props = {
children: <div>Children</div>,
}
})
describe('Check rendered component: ', () => {
it('should render a component successfully', () => {
component = shallow(<ContextMenu {...props}/>)
})
it('should show Menu.Item', () => {
props = {
...props,
menu: [{key: '', title: 'title1'}]
}
component = shallow(<ContextMenu {...props}/>)
});
it('should show Menu.Divider', () => {
props = {
...props,
menu: [{key: 'menu.divider', title: 'title1'}]
}
component = shallow(<ContextMenu {...props}/>)
});
afterEach(() => {
expect(component).toMatchSnapshot()
})
})
})
I run the tests successfully.
However I fail to get the coverage data of the target code with WebStorm's Code Coverage feature below.
From the screenshot below, it seems that WebStorm does not calculate code coverage for the target code while it calculates code coverage for other files.
I am the person who proposals the question.
Investigation
I run the command below in the terminal and find that the code coverage is not calculated for the same files.
jest --coverage
So it is not a specific issue about WebStorm. It is just an issue about Jest config.
Solution
Find the Jest config file: jest.config.js
module.exports = {
verbose: true,
testMatch: ['<rootDir>src/**/*\\.test\\.(ts|tsx|js|jsx)'],
globals: { __DEV__: true },
modulePaths: ['./src'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'],
moduleNameMapper: {
'\\.(css|less|scss|gif)$': 'identity-obj-proxy',
'^.+\\.(css|scss)$': '<rootDir>/node_modules/jest-css-modules',
'\\.(css|less|scss|sass|gif)$': 'identity-obj-proxy',
'^antd/es': 'identity-obj-proxy',
'^#mstr/': 'identity-obj-proxy',
'^dnd-core$': 'dnd-core/dist/cjs',
'^react-dnd$': 'react-dnd/dist/cjs',
'^react-dnd-html5-backend$': 'react-dnd-html5-backend/dist/cjs',
'^react-dnd-touch-backend$': 'react-dnd-touch-backend/dist/cjs',
'^react-dnd-test-backend$': 'react-dnd-test-backend/dist/cjs',
'^react-dnd-test-utils$': 'react-dnd-test-utils/dist/cjs',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'identity-obj-proxy',
},
testResultsProcessor: './scripts/jest-results-processor.js',
coverageReporters: ['lcov', 'text', 'json-summary', 'cobertura'],
setupFiles: ["jest-canvas-mock"],
setupFilesAfterEnv: ['./jest.setup.js'],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/assets/**/*.{js,jsx,json,ts,tsx}',
'!src/lib/!(Canvas)/**/*.{js,jsx,json,ts,tsx}',
'!**/node_modules/**',
'!**/vendor/**',
'!**/env/**',
],
testPathIgnorePatterns: ['<rootDir>/src/components/xx/__tests__/data'],
snapshotSerializers: ['enzyme-to-json/serializer'],
}
The key is the code below
'!src/lib/!(Canvas)/**/*.{js,jsx,json,ts,tsx}',
Change this line to:
'src/lib/**/*.{js,jsx,json,ts,tsx}',
So the code coverage of all files, including the files I am working on, under the lib directory will be calculated.

Resources