We are using NextJS and Material-UI for our site, and upon loading the pages, it gives a FOUC. I've narrowed the problem down to the fact that the JS loads faster than the .css file, so I was wondering if there was a way to preload the .css file? All of our pages use the same .css file which is located under /pages/styles.css
Here is /pages/_app.js if that's any help:
// pages/_app.js
import { Provider } from 'next-auth/client'
import { createMuiTheme } from '#material-ui/core/styles';
import { ThemeProvider } from '#material-ui/styles';
import styles from './styles.css'
import Layout from '../components/layout'
import Head from 'next/head'
const theme = createMuiTheme({
palette: {
primary: {
main: "#2196f3", // blue
},
secondary: {
main: "#d3d3d3", // gray
},
},
});
export default function _App ({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<Provider options={{ clientMaxAge: 0, keepAlive: 0 }} session={pageProps.session}>
<Layout>
{/* Head */}
<Head>
<title>Kevin Support</title>
<link rel="icon" href="/static/favicon.png"/>
</Head>
{/* Page */}
<Component {...pageProps} />
</Layout>
</Provider>
</ThemeProvider>
)
}
Load the CSS file with the <link> element inside the head. The parsing process of the browser will then make sure that the CSS file is loaded before the site content is shown.
In your current approach you load the CSS is loaded with JavaScript, after the FCP has rendered the CSS will be parsed.
You have 2 options to fix this:
You link the CSS file as mentioned above with a <link> element.
You get the text content of the CSS file and set it as the innerHTML of a <style> element.
Perhaps styles weren't applied on the server-side. Try to add _document.js from Material-UI's Next.js example. Adjust it to your needs.
// pages/_document.js
import React from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheets } from '#material-ui/core/styles';
import theme from '../src/theme';
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{/* PWA primary color */}
<meta name="theme-color" content={theme.palette.primary.main} />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// Resolution order
//
// On the server:
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. document.getInitialProps
// 4. app.render
// 5. page.render
// 6. document.render
//
// On the server with error:
// 1. document.getInitialProps
// 2. app.render
// 3. page.render
// 4. document.render
//
// On the client
// 1. app.getInitialProps
// 2. page.getInitialProps
// 3. app.render
// 4. page.render
// Render app and page and get the context of the page with collected side effects.
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
// Styles fragment is rendered after the app and page rendering finish.
styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()],
};
};
Also, you can try to remove server-side injected CSS in the _app.js like so (see example):
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
I faced this same issue.
what I did was to make this work was to add this script inside _document.tsx
<script
dangerouslySetInnerHTML={{
__html: `
if(document) {
document.querySelectorAll("link[rel='preload'][as='style']").forEach(link => link.rel = "stylesheet")}
`
}}
/>
Related
I am struggling to collect custom Simple Analytics metadata in my Next.js app. Looking at their docs, I can either set metadata on the window object (link) or add it via a callback function (link).
My Next.js app looks as follows:
_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
[...snip...]
<script dangerouslySetInnerHTML={{
__html: `window.sa_event=window.sa_event||function(){a=[].slice.call(arguments);sa_event.q?sa_event.q.push(a):sa_event.q=[a]};`
}}/>
</Head>
<body>
<Main />
<NextScript />
// NOTE 3: Where can I define `func` so I have access to the router/query string params?
<script async defer data-metadata-collector="func" src="https://scripts.simpleanalyticscdn.com/latest.js"></script>
<noscript><img src="https://queue.simpleanalyticscdn.com/noscript.gif" alt=""/></noscript>
</body>
</Html>
)
}
}
page.js
import Head from 'next/head'
import { useRouter } from 'next/router'
import Layout from '../components/layout'
export default function Page() {
const router = useRouter()
const i = router.query.i
return (
<>
<Head>
[...snip...]
</Head>
// NOTE 1: This does not work
<script>
sa_metadata = { i: i }
</script>
// NOTE 2: I cannot access `i` here
<script dangerouslySetInnerHTML={{
__html: `window.sa_metadata={ i: i };`
}}/>
[...snip...]
</>
)
}
As you can see, I tried two ways of setting metadata on window (NOTES 1 and 2) and I got stuck on the callback function (NOTE 3). Would appreciate any help in moving this forward.
I'm trying to incorporate the Google Sign In feature in my Next app. Here's how I've been doing it.
In _document.js
import React from 'react';
import Document, {Html, Head, Main, NextScript } from 'next/document';
export default class MyDocument extends Document{
render(){
return(
<Html lang="en">
<Head>
<meta name="theme-color" />
{/* This should add `google` to `window` */}
<script type="application/javascript" src="https://accounts.google.com/gsi/client" async />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
And then in pages/login.js
import { React, useEffect, ... } from 'react'
export default function LoginPage (props) {
// When page is rendered, render the 'Sign-in with Google' button
useEffect(() => {
window.google.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
callback: res => { console.log(res) }
})
window.google.accounts.id.renderButton(
document.getElementById('googleSignIn'),
{ theme: 'filled_blue', size: 'large', text: 'continue_with' }
)
}, [])
return (<>
{/* Provide an element for the button to render into */}
<div id="googleSignIn" />
</>)
}
But this throws an error:
login.js:48 Uncaught TypeError: Cannot read properties of undefined (reading 'accounts')
In other words, window.google is not defined.
What's wrong with this?
You can remove the async attribute from the script to ensure it gets loaded synchronously as early as possible.
<script type="application/javascript" src="https://accounts.google.com/gsi/client" />
Alternatively, you can also use the next/script component with the beforeInteractive strategy to achieve a similar behaviour.
import Script from 'next/script'
<Script type="application/javascript" src="https://accounts.google.com/gsi/client" strategy="beforeInteractive" />
NextJS always prerender pages on server, in this case window is unavailable. You can always use next/router library. to wait until page loads on client
import { React, useEffect, ... } from 'react'
import { useRouter } from 'next/router'
export default function LoginPage (props) {
// When page is rendered, render the 'Sign-in with Google' button
const router=useRouter() //create router state
useEffect(() => {
if(window){ //check window if exist on each effect execution
window.google.accounts.id.initialize({
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
callback: res => { console.log(res) }
})
window.google.accounts.id.renderButton(
document.getElementById('googleSignIn'),
{ theme: 'filled_blue', size: 'large', text: 'continue_with' }
)
}
}, [router]) // to run again when client router loads
return (<>
{/* Provide an element for the button to render into */}
<div id="googleSignIn" />
</>)
}
before this line in your code "window.google.accounts.id.initialize({" try using /global google/ and then try to run it.
enter image description here
I want to add my custom class to some pages. for example
all pages must be this class fixed-header exception this routes:
/cart/step-1
/login
this class add or remove to body element.
<body className="bg-gray fixed-header"
but I don't know how I can handle this scenario?
The easiest and quickest solution. Add this code into each component where you want different classes on the <body>.
useEffect( () => { document.querySelector("body").classList.add("bg-gray fixed-header") } );
Create a custom _document.js and _app.js in your pages directory.
A small util to check if class exists on body (to avoid duplicate class, thanks to suggestion by #juliomalves):
// ./utils/hasClasses
const hasClasses = () =>
document.body.classList.contains("bg-gray") &&
document.body.classList.contains("fixed-header");
export default hasClasses;
Server Side rendering
In _document.js, use the __NEXT_DATA__ prop to get access to the current page,
check if the page is in your allowed routes, and add the classes to body.
import Document, { Html, Head, Main, NextScript } from "next/document";
class MyDocument extends Document {
// Add more routes here if you want to use the same classes
allowedRoutes = ["/login", "/cart/step-1"];
getColor() {
const { page } = this.props?.__NEXT_DATA__;
if (this.allowedRoutes.includes(page))
return "bg-gray fixed-header";
return "";
}
render() {
return (
<Html>
<Head />
<body className={this.getColor()}>
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;
The above code always runs on the server. Classes doesn't get appended to the body on client-side navigation.
Client side rendering
To fix the above issue, use the same logic in _app.js in a useEffect, so that it adds the correct class when rendering on the client.
import { useEffect } from "react";
import { useRouter } from "next/router";
import "../styles.css";
import hasClasses from "./utils/hasClasses";
function MyApp({ Component, pageProps }) {
const { pathname: page } = useRouter();
const allowedRoutes = ["/login", "/cart/step-1"];
useEffect(() => {
if (!hasClasses() && allowedRoutes.includes(page))
document.body.className += "bg-gray fixed-header";
else if (hasClasses()) {
// Don't want the styles in other pages, remove if exists.
// Up to your implementation on how you want to handle this.
document.body.classList.remove("bg-gray");
document.body.classList.remove("fixed-header");
}
});
return <Component {...pageProps} />;
}
export default MyApp;
This solves the issue where client side navigation correctly applies the class on the allowed route. The code in _document.js makes sure that when a page is server rendered, it is sent downstream with the correct class applied so that it doesn't cause a flash of incorrect styles on the client.
I'm creating a whatsapp clone using next.js. On the first load of app i'm getting this error.
Warning: Expected server HTML to contain a matching <div> in <div>.
at div
at O (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1621219033615:37232:19450)
at div
at Paper (http://localhost:3000/_next/static/chunks/pages/chat.js?ts=1621219033615:45091:23)
at WithStyles (http://localhost:3000/_next/static/chunks/pages/chat.js?ts=1621219033615:64751:31)
at div
at Drawer (http://localhost:3000/_next/static/chunks/pages/chat.js?ts=1621219033615:33839:29)
at WithStyles (http://localhost:3000/_next/static/chunks/pages/chat.js?ts=1621219033615:64751:31)
at SideBar (http://localhost:3000/_next/static/chunks/pages/chat.js?ts=1621219033615:67329:75)
at div
at O (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1621219033615:37232:19450)
at Chat (http://localhost:3000/_next/static/chunks/pages/chat.js?ts=1621219033615:73282:70)
at SideMenuProvider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1621219033615:25916:23)
at MyApp (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1621219033615:31532:24)
at ErrorBoundary (http://localhost:3000/_next/static/chunks/main.js?ts=1621219033615:726:47)
at ReactDevOverlay (http://localhost:3000/_next/static/chunks/main.js?ts=1621219033615:829:23)
at Container (http://localhost:3000/_next/static/chunks/main.js?ts=1621219033615:8388:5)
at AppContainer (http://localhost:3000/_next/static/chunks/main.js?ts=1621219033615:8876:24)
at Root (http://localhost:3000/_next/static/chunks/main.js?ts=1621219033615:9012:25)
I'm totally unaware of the source of this error. and they also did not specified from which file, the error is occurred.
Because of this error App UI gets shatter from this
Actual UI of APP
to this
Errored UI of APP
If anybody have any idea why this is happening please help me.
_document.js code here
import React from "react";
import Document, { Html, Main, NextScript, Head } from "next/document";
import { ServerStyleSheets } from "#material-ui/core/styles";
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{/* Meta Tags for SEO */}
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Comatible" content="IE=edge" />
<meta
name="description"
content="A WhatsApp clone made using next js and firebase."
/>
<meta name="keywords" content="WhatsApp Clone" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
MyDocument.getInitialProps = async (ctx) => {
const sheets = new ServerStyleSheets();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheets.collect(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: [
...React.Children.toArray(initialProps.styles),
sheets.getStyleElement(),
],
};
};
You may need to import your components dynamically:
const MyDynamicComponent = dynamic(() => import('./myComponent'))
I'm building a React application and I started using CRA. I configured the routes of the app using React Router. Pages components are lazy-loaded.
There are 2 pages: Home and About.
...
const Home = lazy(() => import('./Home'));
const About = lazy(() => import('./About'));
...
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/about" component={About} />
<Route path="/" component={Home} />
</Switch>
</Suspense>
...
Each page uses the Button component below.
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} ${className}`}>{children}</button>
);
export default Button;
The Button.module.scss file just sets the background color of the button to red.
.btn {
background: red;
}
The Button component accepts a className prop which is then added to the rendered button. This is because I want to give freedom to the consumer of the component. For example, in some pages margins could be needed or the background should be yellow instead of red.
To make it simple, I just want to have a different background color for the Button based on the current page, so that:
Home page => Blue button
About page => Yellow button
Each page is defined as below:
import React from 'react';
import Button from './Button';
import styles from './[PageName].module.scss';
const [PageName] = () => (
<div>
<h1>[PageName]</h1>
<Button className={styles.pageBtn}>[ExpectedColor]</Button>
</div>
);
export default [PageName];
where [PageName] is the name of the page and [ExpectedColor] is the corresponding expected color based on the above bullet list (blue or yellow).
The imported SCSS module, exports a class .pageBtn which sets the background property to the desired color.
Note: I could use a prop on the Button component which defines the variant to display (Blue/Yellow) and based on that prop add a class defined in the SCSS file. I don't want to do that since the change could be something that doesn't belong to a variant (e.g. margin-top).
The problem
If I run the application using yarn start, the application works fine. However, if I build the application (yarn build) and then I start serving the application (e.g. using serve -s build), the behavior is different and the application doesn't work as expected.
When the Home page is loaded, the button is correctly shown with a blue background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.Home_pageBtn__nnyWK {
background: blue
}
That's fine. Then I click on the navigation link to open the About page. Even in this case, the button is shown correctly with a yellow background. Inspecting the loaded CSS chunk, it contains:
.Button_btn__2cUFR {
background: red
}
.About_pageBtn__3jjV7 {
background: yellow
}
When I go back to the Home page, the button is now displayed with a red background instead of yellow. That's because the About page has loaded the CSS above which defines again the Button_btn__2cUFR class. Since the class is now after the Home_pageBtn__nnyWK class definition, the button is displayed as red.
Note: the Button component is not exported on the common chunk because its size is too small. Having that in a common chunk could solve the problem. However, my question is about small shared components.
Solutions
I have thought to 2 solutions which, however, I don't like too much:
Increase selectors specificity
The classes specified in the [PageName].module.scss could be defined as:
.pageBtn.pageBtn {
background: [color];
}
This will increase the selector specificity and will override the default Button_btn__2cUFR class. However, each page chunk will include the shared components in case the component is quite small (less than 30kb). Also, the consumer of the component has to know that trick.
Eject and configure webpack
Ejecting the app (or using something like react-app-rewired) would allow specifying the minimum size for common chunk using webpack. However, that's not what I would like for all the components.
To summarize, the question is: what is the correct working way of overriding styles of shared components when using lazy-loaded routes?
You can use the following logic with config file for any pages. Also, You can send config data from remote server (req/res API) and handle with redux.
See Demo: CodeSandBox
create components directory and create files like below:
src
|---components
|---Button
| |---Button.jsx
| |---Button.module.css
Button Component:
// Button.jsx
import React from "react";
import styles from "./Button.module.css";
const Button = props => {
const { children, className, ...otherProps } = props;
return (
<button className={styles[`${className}`]} {...otherProps}>
{children}
</button>
);
};
export default Button;
...
// Button.module.css
.Home_btn {
background: red;
}
.About_btn {
background: blue;
}
create utils directory and create AppUtils.js file:
This file handle config files of pages and return new object
class AppUtils {
static setRoutes(config) {
let routes = [...config.routes];
if (config.settings) {
routes = routes.map(route => {
return {
...route,
settings: { ...config.settings, ...route.settings }
};
});
}
return [...routes];
}
static generateRoutesFromConfigs(configs) {
let allRoutes = [];
configs.forEach(config => {
allRoutes = [...allRoutes, ...this.setRoutes(config)];
});
return allRoutes;
}
}
export default AppUtils;
create app-configs directory and create routesConfig.jsx file:
This file lists and organizes routes.
import React from "react";
import AppUtils from "../utils/AppUtils";
import { pagesConfig } from "../pages/pagesConfig";
const routeConfigs = [...pagesConfig];
const routes = [
...AppUtils.generateRoutesFromConfigs(routeConfigs),
{
component: () => <h1>404 page not found</h1>
}
];
export default routes;
Modify index.js and App.js files to:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
rootElement
);
...
react-router-config: Static route configuration helpers for React
Router.
// App.js
import React, { Suspense } from "react";
import { Switch, Link } from "react-router-dom";
import { renderRoutes } from "react-router-config";
import routes from "./app-configs/routesConfig";
import "./styles.css";
export default function App() {
return (
<div className="App">
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
</ul>
<Suspense fallback={<h1>loading....</h1>}>
<Switch>{renderRoutes(routes)}</Switch>
</Suspense>
</div>
);
}
create pages directory and create files and subdirectory like below:
src
|---pages
|---about
| |---AboutPage.jsx
| |---AboutPageConfig.jsx
|
|---home
|---HomePage.jsx
|---HomePageConfig.jsx
|
|---pagesConfig.js
About Page files:
// AboutPage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const AboutPage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>about page</h1>
<Button className={btnClass}>about button</Button>
</>
);
};
export default AboutPage;
...
// AboutPageConfig.jsx
import React from "react";
export const AboutPageConfig = {
settings: {
layout: {
config: {
buttonClass: "About_btn"
}
}
},
routes: [
{
path: "/about",
exact: true,
component: React.lazy(() => import("./AboutPage"))
}
]
};
Home Page files:
// HomePage.jsx
import React from "react";
import Button from "../../components/Button/Button";
const HomePage = props => {
const btnClass = props.route.settings.layout.config.buttonClass;
return (
<>
<h1>home page</h1>
<Button className={btnClass}>home button</Button>
</>
);
};
export default HomePage;
...
// HomePageConfig.jsx
import React from "react";
export const HomePageConfig = {
settings: {
layout: {
config: {
buttonClass: "Home_btn"
}
}
},
routes: [
{
path: "/",
exact: true,
component: React.lazy(() => import("./HomePage"))
}
]
};
...
// pagesConfig.js
import { HomePageConfig } from "./home/HomePageConfig";
import { AboutPageConfig } from "./about/AboutPageConfig";
export const pagesConfig = [HomePageConfig, AboutPageConfig];
Edited section:
With HOC Maybe this way: CodeSandBox
create hoc dir and withPage.jsx file:
src
|---hoc
|---withPage.jsx
...
// withPage.jsx
import React, { useEffect, useState } from "react";
export function withPage(Component, path) {
function loadComponentFromPath(path, setStyles) {
import(path).then(component => setStyles(component.default));
}
return function(props) {
const [styles, setStyles] = useState();
useEffect(() => {
loadComponentFromPath(`../pages/${path}`, setStyles);
}, []);
return <Component {...props} styles={styles} />;
};
}
And then pages like below:
src
|---pages
|---about
| |---About.jsx
| |---About.module.css
|
|---home
|---Home.jsx
|---Home.module.css
About.jsx file:
// About.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const About = props => {
const {styles} = props;
return (
<button className={styles && styles.AboutBtn}>About</button>
);
};
export default withPage(About, "about/About.module.css");
About.module.css file:
// About.module.css
.AboutBtn {
background: yellow;
}
Home.jsx file:
// Home.jsx
import React from "react";
import { withPage } from "../../hoc/withPage";
const Home = props => {
const { styles } = props;
return <button className={styles && styles.HomeBtn}>Home</button>;
};
export default withPage(Home, "home/Home.module.css");
Home.module.css file:
// Home.module.css
.HomeBtn {
background: red;
}
I would suggest instead of adding both the default styles and the consumer styles, use the consumer's styles over yours and use your as a callback if not supplied. The consumer can still compose your defaults with the composes keyword.
Button.js
import React from 'react';
import styles from './Button.module.scss';
const Button = ({ children, className}) => (
<button className={className ?? styles.btn}>{children}</button>
);
export default Button;
SomePage.module.scss
.pageBtn {
// First some defaults
composes: btn from './Button.module.scss';
// And override some of the defautls here
background: yellow;
}
If you wish, use sass #extends or #mixin instead
EDIT: Haven't tested it, but could it be that just by using composes webpack will make sure to bundle the defaults only once? Thus you're no longer needed to change your Button.js code with the ??
Solution 1
I know this is very obvious, but would work anyway:
Set !important on your overwriting css rules, thus bypassing specificity:
[PageName].module.scss:
.btn {
color: yellow !important;
}
However, most of the strict devs I know would avoid this keyword at all cost.
Why ?
Because when you start to have a lot of !important your css is a nightmare to debug. If you start writing !important rules with higher specificity, you know you have gone too far
It is only meant for corner-cases like yours, you might as well use it.
Solution 2
fix CRA config to enforce style tags order.
It is open-source after all :)
You can give your input on this bug here (upvote might give it more visibility):
https://github.com/facebook/create-react-app/issues/7190
Solution 3 (Update)
You could create a SCSS mixin in a new customButton.scss file, to generate css rules with higher specificity:
// customButton.scss
#mixin customBtn() {
:global {
.customBtn.override {
#content;
}
}
}
We will use two static class names (using the :global selector), because that way their name won't change based on where they are imported from.
Now use that mixin in your pages' SCSS:
// [pageName].module.scss
#import 'customButton.scss';
#include customBtn {
color: yellow;
}
css output should be:
.customBtn.override {
// put everything you want to customize here
color: yellow;
}
In Button.jsx: apply both class names to your button in addition to styles.btn:
// Button.jsx
const Button = ({ children, className = '' }) => (
<button className={`${styles.btn} customBtn override ${className}`}>
{children}
</button>
);
(Note that these are not referenced through the styles object, but the classname directly)
The main drawback is these are not dynamic class names, so you have to watch out to avoid conflicts yourself like we use to do before.
But I think it should do the trick