Tree shakable icon component - reactjs

I am creating a custom design system on top of ChakraUI and bootstrap icons.
Bootstrap icons contains 1000+ svg icons.
I created an abstract Icon component to have abstraction over the icon name, that seems IMO more convenient.
However, I am not sure this module will be tree-shaked, and I can't figure out how to change the implementation to make it tree-shakable.
Icon.tsx
import React from 'react'
import * as bootstrapIcons from '#emotion-icons/bootstrap'
// Union of all icon names: 'Alarm' | 'Circle' | 'Square' ...
type IconNames = keyof typeof bootstrapIcons
export const Icon: FC<{name: IconNames}> = ({name}) => {
const Icon = bootstrapIcons[name]
return (
<div>
<Icon style={{...}} />
</div>
)
}
App.tsx
import React from 'react'
import { Icon } from './Icon'
const App = () => {
return <div>
<Icon name="Alarm" />
</div>
}

Astrix imports cannot be tree shaken.
Generally when it comes to Icons you can have a Sprite Sheet or Tree Shaking but not both.
To make this particular library tree shake out icons you'll have to change your imports and usage to look like this (haven't tested but should work).
import { Alarm } from '#emotion-icons/bootstrap/Alarm';
const Use = () => <Alarm/>
There's definitely a caveat here where it would be possible to build a build-time plugin that changes...
<Icon name="Alarm"/>
// to.
import { Alarm } from '#emotion-icons/bootstrap/Alarm';
<Alarm/>
and removes the import.
The reason why astrix imports can't be tree shaken is because javascript is a very dynamic language and static analysis of which keys are used is sometimes very difficult.
Consider the following code
import * as bootstrapIcons from '#emotion-icons/bootstrap'
bootstrapIcons["mralA".reverse()] // which icons should be tree shaken?
const Icons = new Proxy(bootstrapIcons) // which icons should be included? using proxy traps.
const SubSet = PullSubset(bootstrapIcons) // which icons should be included? no matter what the implementation of PullSubset is it will be impossible to preform static analysis on it to determine which icons have been used.

Related

React testing library can't read styles using Tailwind CSS classes

I have a simple React component that will initially have a Tailwind CSS class of hidden which apply CSS display: none and will change the class to visible on button click.
When I test with expect().not.toBeVisible() it tells me the element is already visible while it has a hidden class.
If I don't use Tailwind CSS and use a normal style={{display: 'none'}} it'll correctly identify that the element isn't visible. That means clearly the issue is with Tailwind CSS.
Here's my test:
test("Notification bar should be initially hidden but visible on click", async () => {
render(<Notifications />);
expect(await screen.findByTestId("list")).not.toBeVisible();
// this test fails while the element already has a Tailwind CSS class of "hidden"
});
While this's my component:
<ul className="hidden" data-testid="list">
<li>item 1</li>
</ul>
The solution explained in this Stack Overflow: cannot check expectelm not tobevisible for semantic ui react component. Based on that thread, I extend the solution to make it works with TailwindCSS as the steps explained below,
Project structure
root/
src/
test/
index.css
test-utils.tsx
component.test.tsx
index.css
1. Generate CSS from the TailwindCSS template files
By issuing the command below, the CSS file called index.css will be generated in src/test directory
npx tailwindcss -i ./src/index.css -o ./src/test/index.css
Further reading: TailwindCSS installation
2. Create custom render function
Next we need to inject the generated CSS file into the JSDOM. Custom render function will be useful so we won't be needed to repeat this task for each test
import { render, RenderOptions } from '#testing-library/react';
import React, { FC, ReactElement } from 'react';
import fs from 'fs';
const wrapper: FC<{ children: React.ReactNode }> = ({ children }) => {
return <>{children}<>;
};
const customRender = (ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) => {
const view = render(ui, { wrapper, ...options });
const style = document.createElement('style');
style.innerHTML = fs.readFileSync('src/test/index.css', 'utf8');
document.head.appendChild(style);
return view;
};
export * from '#testing-library/react';
export { customRender as render };
Further reading: Testing Library Setup
3. Perform testing, unit test suppose to be success now
import React from 'react';
import { render, screen } from './test-utils';
test('Renders hidden hello world', () => {
render(<span className="hidden">Hello World</span>);
expect(screen.getByText('Hello World')).not.toBeVisible();
});
Why souldn't we use toHaveClass matchers instead?
it wouldn't align with the Testing Library guiding principle of “emphasize a focus on tests that closely resemble how your web pages are interacted by the users“ because by doing so, you are interacting with the component unnaturally

Some react Lottie Animations code-splitted with loadable crash on Gatsby development but work in prod

in our project we have around 10 animations that are using react-lottie library and are loaded with loadable-component library. Some of these animations are crashing on Gatsby development environment. I have managed to narrow down which ones are failing on my end and there were 2.
The component is built like this:
#LottieComponent.tsx
import React, { CSSProperties } from 'react';
import Lottie from 'lottie-react';
import json from './lottieAnimation.json';
interface Props {
styles?: CSSProperties;
}
export default ({ styles }: Props) => (
<Lottie style={styles} animationData={json} />
);
Then we use code splitting:
#index.tsx
import loadable from '#loadable/component';
const ExchangeInfographic = loadable(
() => import('./animationComponents/ExchangeGraphic')
);
export {
ExchangeInfographic
};
Then we import the component into the module like this:
import { ExchangeInfographic } from '../../components/Infographic';
const ExampleComponent = () => {
const [animationWasRendered, setAnimationWasRendered] = useState(false);
return (
<ReactVisibilitySensor
onChange={(isVisible) => {
isVisible && setAnimationWasRendered(true);
}}
partialVisibility
>
<SectionCustomComponent>
<Container>
<Col>
{animationWasRendered ? <ExchangeInfographic /> : <></>}
</Col>
</Row>
</Container>
</Section>
</ReactVisibilitySensor>
);
};
export default ExampleComponent;
The error that I get in the console is:
react-dom.development.js:23966 Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Check the render method of `InnerLoadable`.
I have check the possible reasons for this error message and found this thread:
https://github.com/facebook/react/issues/13445
and
Code splitting/react-loadable issue
but it doesn't look as if the dynamic import would fail. The confusing part is that, why would it only crash on some animations and the other work fine every time. O.o
Would you have an idea what am I missing?
Thanks for any suggestions!
I'm not sure if it will fix the issue because I don't know all the specs nor your use-case, but this export seems odd to me:
#index.tsx
import loadable from '#loadable/component';
const ExchangeInfographic = loadable(
() => import('./animationComponents/ExchangeGraphic')
);
export {
ExchangeInfographic
};
Have you tried something like:
#index.tsx
import loadable from '#loadable/component';
const ExchangeInfographic = loadable(
() => import('./animationComponents/ExchangeGraphic')
);
export ExchangeInfographic
So after few months I decided to give it another try at this weird bug.
Explanation of the issue:
ExchangeInfographic react component had a file name like this ExchangeInfographic.tsx
The json file containing json for the lottie animation had a file name like this exchangeInfographic.json
For some reason when the name of the json file and react component file was the same (except for capital letter for react component) after importing the component like this
import {ExchangeInfographic} from 'path to the file;'
It would return an object instead of function.
To fix it, I just changed the name of the file where json was to for example exchange.json
It's a kind of magic. It's a kind of maaagiiic...

How to load only needed CSS per component (React js)

I've started to code my first React app and it's awesome, but I can't figure out how to manage css files per-component(so the actual CSS won't load if it is not necessary).
React with webpack(correct me if I'm wrong please) wraps the project in such a way that at every given moment the app loads only what it needs(in terms of JS).
So if I have my main App component with only two buttons visible: btn-01 and btn-02, and inside of this component I have another two: component-01 and component-02, and they are hidden till the corresponded button is clicked(btn-01 for component-01), these components won't be loaded until the actual button is clicked(am I getting this right?), however this is not the same with css as I can tell, because I see the css of each of these(component-01 and component-02) components loaded right away the App is loaded, even though none of the buttons are clicked.
I'm not a big fan of inline styling, but I did test it with css module, but the result is the same in this aspect. So I'm not even sure if this is possible to implement in an easy way.
Here's a code, so perhaps I'm not implementing it correctly, but please don't mind the none-DRY code etc.
So as you may see, the style of Component-01 and -02 are loaded even though there is no need for them at the moment(none of the button is pressed).
App.js
import React, { Component } from "react";
import "./App.css";
import Component_01 from "./Component-01/Component-01";
import Component_02 from "./Component-02/Component-02";
class App extends Component {
state = {
isComponent_01: false,
isComponent_02: false,
};
toggleComponent01 = () => {
this.setState({
isComponent_01: !this.state.isComponent_01,
});
};
toggleComponent02 = () => {
this.setState({
isComponent_02: !this.state.isComponent_02,
});
};
render() {
let showComponent_01 = null;
if (this.state.isComponent_01) {
showComponent_01 = <Component_01 />;
}
let showComponent_02 = null;
if (this.state.isComponent_02) {
showComponent_02 = <Component_02 />;
}
return (
<div className="App">
<button className="btn-01" onClick={this.toggleComponent01}>
Btn-01
</button>
<button className="btn-02" onClick={this.toggleComponent02}>
Btn-02
</button>
{showComponent_01}
{showComponent_02}
</div>
);
}
}
export default App;
Component-01.js (and Component-02.js, just with -02.js)
import React from "react";
import style from "./Component-01.module.css";
function App() {
return <div className={style["component-01"]}>Component-01</div>;
}
export default App;

React-JSS with material-ui and server side rendering

I have a react application and a custom component library built using material-ui. Components within the react application itself uses react-jss for styling and I'd like to avoid too many references to material-ui in the app itself just in case I wish to replace material-ui in the future.
From what I've read it seems that Material-UI uses a wrapped version of JSS so they should be compatible, but I'm unable to extract both my own styles as well as those from material-ui. I seem to be able to extract one or the other though...
I've followed the React-JSS Server Side Rendering guide and that works fine, but I'm wondering how I can most easily combine styles from both without having to resort to using #material-ui/core/styles everywhere.
Here is my bootstrap component that registers the JssProvider in my app:
export const Bootstrap: React.FC<IBootstrapProps> = ({
sheetsRegistry,
generateId
}) => {
return (
<JssProvider registry={sheetsRegistry} generateId={generateId}>
<App />
</JssProvider>
)
}
export default Bootstrap
The sheetsRegistry and generateId props comes from a render function called directly from an express handler:
const render = (props: ISSRRenderProps) => {
const sheetsRegistry = new SheetsRegistry()
const generateId = createGenerateId()
const appContent = renderToString(
<Bootstrap
sheetsRegistry={sheetsRegistry}
generateId={generateId}
/>
)
return {
appContent,
styles: sheetsRegistry.toString()
}
}
This gives me all my custom styles in the returned styles property which I in turn dump to the page without issues. The problem comes when I try to combine this with #material-ui. Simply including a component from the library does not give me it's styles server side though they do render on the client after a FOUC.
Using the server rendering guide from material-ui and modifying my Bootstrap component does give my the material-ui styles, but now my own styles are not collected. This also adds a single reference to material-ui in my react app, but if that is what it takes I can live with it as long as sub-components don't have to use material-uis style tools.
import { ServerStyleSheets } from "#material-ui/core/styles"
export const Bootstrap: React.FC<IBootstrapProps> = ({
sheetsRegistry,
generateId,
...rest
}) => {
const sheets = new ServerStyleSheets()
sheetsRegistry?.add(sheets as any)
return sheets.collect(
<JssProvider registry={sheetsRegistry} generateId={generateId}>
<App {...rest} />
</JssProvider>
)
}
Is there a way to get the best of both worlds? I'd love to use Material-UI components in my UI library and extract all styles globally for server side rendering.
I've been able to come up with a "workaround" by modifying my render function to collect material-ui styles separately and just concatenating them:
import { ServerStyleSheets } from "#material-ui/core/styles"
const render = (props: ISSRRenderProps) => {
const sheetsRegistry = new SheetsRegistry()
const generateId = createGenerateId()
const muiStyles = new ServerStyleSheets()
const appContent = renderToString(
muiStyles.collect(
<Bootstrap
sheetsRegistry={sheetsRegistry}
generateId={generateId}
/>
)
)
return {
appContent,
styles: muiStyles.toString() + sheetsRegistry.toString()
}
}
The reason believe this is a workaround and may not be the best solution is that I still have to separately handle my own styles vs material-ui styles. Seeing as they both use JSS behind the scenes I still think it should be possible to seamlessly collect both in the same sheetsRegistry, but this works for now.

Dynamically loading Material-UI Icons in React

I'm trying to make React component that dynamically imports requested Material-UI icon,
when the page loads. My solution presented here works, but it gives warning
at compile time. Additionally it slows down the compilation of the project.
Any idea on how to do this properly?
https://github.com/jurepetrovic/ordermanager_demo
The main logic is found in App.js, lines 5-10:
import React, { Component } from 'react';
import BarChartIcon from '#material-ui/icons/BarChart';
const MaterialIcon = ({ icon }) => {
console.log("icon: " + icon);
let resolved = require(`#material-ui/icons/${icon}`).default;
return React.createElement(resolved);
}
class App extends Component {
render() {
return (
<div>
<MaterialIcon icon={"PowerSettingsNew"} />
</div>
);
}
}
export default App;
The warning it gives is this:
Compile warning
I finally found the simplest solution to this problem:
import all material icons from the package:
import * as Icons from '#material-ui/icons'
I assume you fetch your icon names from your api and finally you have something like this:
var iconNamesArray = ["FitnessCenter","LocalDrink","Straighten"]
Finally load your Icons like below:
<div className="my-icons-wrapper-css">
{
iconNamesArray.map((el,ind)=>{
let DynamicIcon = Icons[el]
return(
<DynamicIcon className="my-icon-css"/>
);
})
}
</div>
And your icons will appear in the screen.

Resources