Route-based code splitting causes css duplicate - javascript

Using route-based code splitting causes css duplicate in chunks.
Steps to reproduce:
Create simple app with create-react-app
Use react-router-dom for client side routing
Add one common component with local css.
Add 2 pages, using the common component with style overwriting
Run build command
Check static folder
// /components/CommonPageHeader/CommonPageHeader.module.scss
.header {
color: black;
}
// /components/CommonPageHeader/CommonPageHeader.tsx
import styles from "./CommonPageHeader.module.scss";
import { ReactNode } from "react";
export const CommonPageHeader = (props: {
className: string;
children: ReactNode;`your text`
}) => {
const { className = "", children } = props;
return <h1 className={`${styles.header} ${className}`}>{children}</h1>;
};
// /pages/Home/Home.module.scss
.greenHeader {
color: green;
}
// /pages/Home/Home.tsx
import React from "react";
import { Link } from "react-router-dom";
import { CommonPageHeader } from "../../components/CommonPageHeader/CommonPageHeader";
import styles from "./Home.module.scss";
const Home = () => (
<>
<CommonPageHeader className={styles.greenHeader}>
I'm a green header
</CommonPageHeader>
<Link to={"some-page"}>Some page</Link>
</>
);
export default Home;
// /pages/SomePage/SomePage.module.scss
.redHeader {
color: red;
}
// /pages/SomePage/SomePage.tsx
import React from "react";
import { Link } from "react-router-dom";
import { CommonPageHeader } from "../../components/CommonPageHeader/CommonPageHeader";
import styles from "./SomePage.module.scss";
const SomePage = () => {
return (
<>
<CommonPageHeader className={styles.redHeader}>
I'm a red header
</CommonPageHeader>
<Link to={"/"}>Home</Link>
</>
);
};
export default SomePage;
// index.tsx
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
const Home = React.lazy(() => import("./pages/Home/Home"));
const SomePage = React.lazy(() => import("./pages/SomePage/SomePage"));
const router = createBrowserRouter([
{
path: "/",
element: (
<Suspense>
<Home />
</Suspense>
),
},
{
path: "/some-page",
element: (
<Suspense>
<SomePage />
</Suspense>
),
},
]);
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
You get css chunks with duplicates and unexpected styles overwrites in production:
// /build/static/css/499.408c3ff0.chunk.css
.CommonPageHeader_header__eTauC {
color: #000
}
.Home_greenHeader__YwF4H {
color: green
}
// /build/static/css/136.855973ab.chunk.css
.CommonPageHeader_header__eTauC {
color: #000
}
.SomePage_redHeader__a-I8c {
color: red
}
Demo page
Click to some page link
Go back to home page
Repo
Is there way to prevent it?

Related

React.js: How to implement dark/light mode in body toggling with useContext?

I am trying to create a background theme which will switch on onClick. On onClick it must change the background color of body in react app. I've managed to implement useContext, and now it toggles and changes the list items color in Header component. How to set it to body as well? Any help will be appreciated.
Here is my useContext color component
import React from 'react'
export const themes = {
light: {
foreground: '#ffffff',
},
blue: {
foreground: 'blue',
},
}
export default React.createContext({
theme: themes.light,
switchTheme: () => {},
})
onClick Button component
import React, { useContext } from 'react'
import ThemeContext from './context'
import './ThemedButton.scss'
const ThemedButton = () => {
const { switchTheme } = useContext(ThemeContext)
return (
<>
<button className="btn" onClick={switchTheme}>
Switch
</button>
</>
)
}
export default ThemedButton
App.js
import React, { useState } from 'react'
import SearchBar from './components/SearchBar';
import useCountries from './Hooks/useCountries';
import MainTable from './components/MainTable';
import ThemeButton from './useContext/ThemedButton';
import ThemeContext from './useContext/context';
import { searchProps } from './types';
import { themes } from './useContext/context';
import Routes from './Routes';
import './App.scss'
export default function App() {
const [search, setSearch] = useState('')
const [data] = useCountries(search)
const [context, setContext] = useState({
theme: themes.light,
switchTheme: () => {
setContext((current) => ({
...current,
theme: current.theme === themes.light ? themes.blue : themes.light,
}))
},
})
const handleChange: React.ReactEventHandler<HTMLInputElement> = (e): void => {
setSearch(e.currentTarget.value)
}
return (
<div className="App">
<SearchBar handleChange={handleChange} search={search as searchProps} />
<ThemeContext.Provider value={context}>
<ThemeButton />
<MainTable countries={data} />
</ThemeContext.Provider>
<Routes />
</div>
)
}
Header component
import React, { useContext } from 'react'
import ThemeContext from '../../useContext/context'
import './Header.scss'
export default function Header() {
const { theme } = useContext(ThemeContext)
return (
<div className="header">
<ul className="HeadtableRow" style={{ color: theme.foreground }}> // here it's set to change list items color
<li>Flag</li>
<li>Name</li>
<li>Language</li>
<li>Population</li>
<li>Region</li>
</ul>
</div>
)
}
If you want to change your body tag in your application you need to modify DOM and you can add this code to your Header.js (or any other file under your context) file:
useEffect(() => {
const body = document.getElementsByTagName("body");
body[0].style.backgroundColor = theme.foreground
},[])
*** Don't forget to import useEffect
*** Inline style like below is a better approach than modifying DOM directly
<div className="App" style={{backgroundColor: context.theme.foreground}}>
//For under context files just use theme.foreground
<SearchBar handleChange={handleChange} search={search as searchProps} />
<ThemeContext.Provider value={context}>
<ThemeButton />
<MainTable countries={data} />
</ThemeContext.Provider>
<Routes />
</div>

How to detect if another component is present in the document?

I have a site built with React Static that has a Header component that is always present. Depending on if the current page has a hero component or not, the Header should be either light or dark.
The Header is rendered outside of the routes and the useEffect is triggered before the children is rendered. This is probably because of the routing.
This is the current code:
// App.js
import React, { useState, useEffect } from 'react'
import { Root, Routes } from 'react-static'
export default () => {
const [useDarkTheme, setUseDarkTheme] = useState(false);
useEffect(() => {
if (typeof document !== "undefined") {
const heroPresent = document.querySelectorAll(".o-hero").length > 0;
console.log("The hero is present: " + heroPresent);
setUseDarkTheme(!heroPresent);
}
})
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<Header useDarkTheme={ useDarkTheme } />
<Routes default />
</React.Suspense>
</Root>
);
}
What will be rendered at <Routes default /> is the static pages configured in React Static's static.config.js.
Below is an example of the Hero component:
// Hero.js
import React from "react";
export default () => {
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
When I run the application and look at the logs this is what I get:
The hero is present: false
This is the Hero rendering. If this exist, the Header should be dark.
How could I somehow detect the presence of the Hero from the Header although the Hero is in a router and the Header is not? This feels like quite a common use case, but I could not find any info on the interwebs.
Thanks in advance!
So I ended up using useContext to provide all children with a getter and a setter for the Header's theme (dark or light). The solution is very much inspired from this answer. The solution looks like this:
// App.js
import React, { useState, useContext } from 'react'
import { Root, Routes } from 'react-static'
import { HeaderThemeContext } from "./context";
export default () => {
const { theme } = useContext(HeaderThemeContext);
const [headerTheme, setHeaderTheme] = useState(theme);
return (
<Root>
<React.Suspense fallback={ <em>Loading...</em> }>
<HeaderThemeContext.Provider value={ { theme: headerTheme, setTheme: setHeaderTheme } }>
<Header theme={ headerTheme } />
<Routes default />
</HeaderThemeContext.Provider>
</React.Suspense>
</Root>
);
}
// Hero.js
import React from "react";
import { headerThemes, setHeaderTheme } from "./context";
export default () => {
setHeaderTheme(headerThemes.DARK);
console.log("This is the Hero rendering. If this exist, the Header should be dark.");
return (
<div className="o-hero">
<p>Hero!</p>
</div>
);
}
// context.js
import React, { createContext, useContext } from "react";
export const headerThemes = {
LIGHT: "light",
DARK: "dark",
};
export const HeaderThemeContext = createContext({
theme: headerThemes.LIGHT,
setTheme: () => {}
});
// This is a hook and can only be used in a functional component with access to the HeaderThemeContext.
export const setHeaderTheme = theme => useContext(HeaderThemeContext).setTheme(theme);
This gives global access to set and get the header theme, which might not be optional, but it works for now and I think it's fine. Please let me know if there is a better way of doing this.

Passing react-router-dom's Link into external library

I'm rendering components from my external (node_modules) pattern library. In my main App, I'm passing my Link instance from react-router-dom into my external libraries' component like so:
import { Link } from 'react-router-dom';
import { Heading } from 'my-external-library';
const articleWithLinkProps = {
url: `/article/${article.slug}`,
routerLink: Link,
};
<Heading withLinkProps={articleWithLinkProps} />
In my library, it's rendering the Link as so:
const RouterLink = withLinkProps.routerLink;
<RouterLink
to={withLinkProps.url}
>
{props.children}
</RouterLink>
The RouterLink seems to render correctly, and even navigates to the URL when clicked.
My issue is that the RouterLink seems to have detached from my App's react-router-dom instance. When I click Heading, it "hard" navigates, posting-back the page rather than routing there seamlessly as Link normally would.
I'm not sure what to try at this point to allow it to navigate seamlessly. Any help or advice would be appreciated, thank you in advance.
Edit: Showing how my Router is set up.
import React from 'react';
import { hydrate, unmountComponentAtNode } from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { ConnectedRouter } from 'react-router-redux';
import RedBox from 'redbox-react';
import { Route } from 'react-router-dom';
import { Frontload } from 'react-frontload';
import App from './containers/App';
import configureStore from './redux/store';
import withTracker from './withTracker';
// Get initial state from server-side rendering
const initialState = window.__INITIAL_STATE__;
const history = createBrowserHistory();
const store = configureStore(history, initialState);
const mountNode = document.getElementById('react-view');
const noServerRender = window.__noServerRender__;
if (process.env.NODE_ENV !== 'production') {
console.log(`[react-frontload] server rendering configured ${noServerRender ? 'off' : 'on'}`);
}
const renderApp = () =>
hydrate(
<AppContainer errorReporter={({ error }) => <RedBox error={error} />}>
<Provider store={store}>
<Frontload noServerRender={window.__noServerRender__}>
<ConnectedRouter onUpdate={() => window.scrollTo(0, 0)} history={history}>
<Route
component={withTracker(() => (
<App noServerRender={noServerRender} />
))}
/>
</ConnectedRouter>
</Frontload>
</Provider>
</AppContainer>,
mountNode,
);
// Enable hot reload by react-hot-loader
if (module.hot) {
const reRenderApp = () => {
try {
renderApp();
} catch (error) {
hydrate(<RedBox error={error} />, mountNode);
}
};
module.hot.accept('./containers/App', () => {
setImmediate(() => {
// Preventing the hot reloading error from react-router
unmountComponentAtNode(mountNode);
reRenderApp();
});
});
}
renderApp();
I've reconstructed your use case in codesandbox.io and the "transition" works fine. So maybe checking out my implementation might help you. However, I replaced the library import by a file import, so I don't know if that's the decisive factor of why it doesn't work without a whole page reload.
By the way, what do you mean exactly by "seamlessly"? Are there elements that stay on every page and should not be reloaded again when clicking on the link? This is like I implemented it in the sandbox where a static picture stays at the top on every page.
Check out the sandbox.
This is the example.js file
// This sandbox is realted to this post https://stackoverflow.com/q/59630138/965548
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import { Heading } from "./my-external-library.js";
export default function App() {
return (
<div>
<img
alt="flower from shutterstock"
src="https://image.shutterstock.com/image-photo/pink-flowers-blossom-on-blue-600w-1439541782.jpg"
/>
<Router>
<Route exact={true} path="/" render={Welcome} />
<Route path="/article/coolArticle" component={CoolArticleComponent} />
</Router>
</div>
);
}
const Welcome = () => {
const articleWithLinkProps = {
url: `/article/coolArticle`,
routerLink: Link
};
return (
<div>
<h1>This is a super fancy homepage ;)</h1>
<Heading withLinkProps={articleWithLinkProps} />
</div>
);
};
const CoolArticleComponent = () => (
<div>
<p>This is a handcrafted article component.</p>
<Link to="/">Back</Link>
</div>
);
And this is the my-external-library.js file:
import React from "react";
export const Heading = ({ withLinkProps }) => {
const RouterLink = withLinkProps.routerLink;
return <RouterLink to={withLinkProps.url}>Superlink</RouterLink>;
};

undefined is not an object (evaluating '_this2.props.navigation.navigate') onPress React-Native

Working with react-native and I get a problem with navigator.
Routes.js
import React from 'react';
import { createStackNavigator, createDrawerNavigator } from 'react-navigation';
import ItemListScreen from '../screens/ItemListScreen';
import ItemDetailsScreen from '../screens/ItemDetailsScreen';
export const RootStack = () => {
return createDrawerNavigator(
{
Home: {
screen: ItemList
},
ItemDetails: {
screen: ItemDetails
}
}
)}
export const ItemList = createStackNavigator({
ItemList: {
screen: ItemListScreen
}
},
{
headerMode: 'none'
});
export const ItemDetails = createStackNavigator({
ItemDetails: {
screen: ItemDetailsScreen
}
},
{
headerMode: 'none'
});
Header.js
import React, { Component } from 'react';
import { StyleSheet } from 'react-native';
import { Header, Body, Text, Icon, Left, Right } from 'native-base';
export default class AppHeader extends Component {
render() {
const headerText = this.props.headerText
return (
<Header>
<Left><Icon name='menu' onPress={()=> this.props.navigation.navigate('DrawerOpen')} /></Left>
<Body style={styles.header}>
<Text style={styles.headerText}>{headerText}</Text>
</Body>
<Right></Right>
</Header>
);
}
}
Index.js
import React, { Component } from 'react';
import { StyleSheet } from 'react-native';
import { Root, Button, Text, Drawer } from 'native-base';
import {RootStack} from './config/Routes';
import Header from './components/Header/Header';
import SideBar from './components/SideBar/SideBar';
export default class Index extends Component {
render() {
const Screen = RootStack();
const { globalContainer } = styles;
return (
<Root style={ globalContainer }>
<Header />
<Screen />
</Root>
)
}
}
The error is:
undefined is not an object (evaluating
'_this2.props.navigation.navigate')
The error is in OnPress() in Header.js
onPress={() => this.props.navigation.navigate('DrawerOpen')
What is the cause of this error? How to solve?
Your navigation object is not defined since you are not providing it the object.
You can include the navigation object using two ways,
Declare the object in the StackNavigator class
Pass navigation props explicitly. For example - in index.js you'll need to change <Header /> to <Header navigation={this.props.navigation} />. So, here you are providing it the necessary proprs in order to execute the navigate action.
EDIT
The actual issue is here,
<Root style={ globalContainer }>
<Header />
<Screen />
</Root>
you are defining your routes later, but calling your Header screen earlier. So precisely, navigation object is undefined in index.js itself.
What you should do is, list index.js in the StackNavigator class as the first object, so it'll be called first. So, your index.js will look something like this.
<Root style={ globalContainer }>
<Header navigation={this.props.navigation} /> //navigation object will be defined here
</Root>
Also, as i see, you have made your DrawerNavigator as your RootStack. I'll like to propose something different, You define a StackNavigator as your root stack, and then include drawer navigation in it.
Something on the lines of -
export const RootStack = createStackNavigator({
Index: //your index.js screen declaration
Drawer: //drawer navigator object
ItemDetails: {
screen: ItemDetailsScreen
}
},
EDIT 2
You'll be not be calling Rootstack in index.js. Your index.js will look something like this.
export default class Index extends Component {
render() {
const { globalContainer } = styles;
return (
<Root style={ globalContainer }>
<Header navigation={this.props.navigation}/>
</Root>
)
}
}
If index.js is your entry file, then you'll have to create a new entry file that calls the RootStack.
Something like entryFile.js
render() { return <RootStack /> }
which will automatically render all your routes and place index.js as your first screen.
Finally solved the issue. My approach has been given below:
Routes.js
import React from 'react';
import { createStackNavigator } from 'react-navigation';
import { Drawer } from './Drawer';
export const App = createStackNavigator(
{
Drawer: {
screen: Drawer
}
},
{
initialRouteName: "Drawer",
headerMode: "none"
}
)
Drawer.js
import React, { Component } from 'react';
import { StyleSheet } from 'react-native';
import { createDrawerNavigator } from 'react-navigation';
import ItemListScreen from '../screens/ItemListScreen';
import SideBar from '../components/SideBar/SideBar';
export const Drawer = createDrawerNavigator(
{
Home: { screen: ItemListScreen }
},
{
navigationOptions: {
gesturesEnabled: false
},
initialRouteName: "Home",
drawerPosition: 'left',
contentComponent: props => <SideBar {...props} />
}
);
Index.js
import React, { Component } from 'react';
import { StyleSheet } from 'react-native';
import { Button, Text, Drawer } from 'native-base';
import { App } from './config/Routes';
import AppHeader from './components/Header/Header';
export default class Index extends Component {
render() {
const { globalContainer } = styles;
return (
<App style={ globalContainer } navigation={this.props.navigation}></App>
)
}
}
Header.js
import React, { Component } from 'react';
import { StyleSheet } from 'react-native';
import { Header, Body, Text, Icon, Left, Right } from 'native-base';
export default class AppHeader extends Component {
render() {
const {navigation, headerText} = this.props
const {header, text, drawerIcon } = styles
return (
<Header>
<Left>
<Icon name='menu' style={drawerIcon} onPress={()=> navigation.openDrawer()} />
</Left>
<Body style={header}>
<Text style={text}>{headerText}</Text>
</Body>
<Right></Right>
</Header>
);
}
}

How to extend or merge Styled Components theme

I have a Styled Components-based component library with its own set of theme settings that for the most part will never need to be overridden. I'm in the process of pulling this component library into another project that also uses Style Components and has its own theme. How can I import components from the component library and this project and ensure that each of them are only provided with theme values from their corresponding repo? I don't want to override my component library theme, I'd like to manage 2 separate themes so that my component library has access to a default theme and this other project can define a separate theme object for it's own components
Example:
Separate Project
const theme = {
colors: {
error: '#f23f3f',
}
}
import { SeparateProjectThemeProvider } from 'separate-proj';
class App extends React.Component {
render () {
return (
<SeparateProjectThemeProvider theme={theme}>
<h1>Hello</h1>
</SeparateProjectThemeProvider>
)
}
}
Component Library
const theme = {
colors: {
brand: '#3bbdca',
}
}
import { ThemeProvider } from "styled-components";
import defaultTheme from "./theme-settings";
const mergeThemes = (theme1, theme2) => {
const mergedTheme = { ...theme1, ...theme2 };
return mergedTheme;
};
const CustomThemeProvider = props => {
const customTheme = {
custom: Object.assign({}, defaultTheme)
};
return (
<ThemeProvider theme={mergeThemes(customTheme, props.theme)}>
{props.children}
</ThemeProvider>
);
};
export default CustomThemeProvider;
The code below will demonstrate a couple options -- either pre-wrapping each of your component-lib components with the appropriate theme provider (WrappedTitle) or wrapping as you use them ("Hello Component World!" portion).
// Sample component-lib/index.js
import React from 'react';
import styled from "styled-components";
import {ThemeProvider} from "styled-components";
const theme = {
titleColor: "green"
};
export const CompLibThemeProvider = props => {
const customTheme = Object.assign({}, theme, props.theme);
return (
<ThemeProvider theme={customTheme}>
{props.children}
</ThemeProvider>
);
};
export const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: ${props => props.theme.titleColor};
`;
export const WrappedTitle = (props) => {
return (<CompLibThemeProvider><Title {...props}/></CompLibThemeProvider>);
};
And here is some sample project code:
// App.js
import React from 'react';
import styled from 'styled-components';
import {Title, CompLibThemeProvider, WrappedTitle} from 'component-lib';
import {ThemeProvider} from "styled-components";
const theme = {
titleColor: "red"
};
export const BigTitle = styled.h1`
font-size: 5em;
text-align: center;
color: ${props => props.theme.titleColor};
`;
const ProjectThemeProvider = props => {
return (
<ThemeProvider theme={Object.assign({}, theme, props.theme)}>
{props.children}
</ThemeProvider>
);
};
const App = () => {
return (
<>
<ProjectThemeProvider>
<>
<BigTitle>Hello Big World!</BigTitle>
<WrappedTitle>Hello Pre-wrapped World!</WrappedTitle>
</>
</ProjectThemeProvider>
<CompLibThemeProvider><Title>Hello Component World!</Title></CompLibThemeProvider>
</>
);
}
export default App;

Categories