I am looking to use Next.js with Material UI with my React app. I have followed the official Material UI Next.js exmaple.
Although, when I try either run next dev or next build, the following error occurs.
I have run npm ls react to ensure I only have one version of react, and confirmed that react-dom and react were the same version (6.13.1).
It seems to be treating the originalRenderPage as a hook. Although, the official Next.js documentation also details the custom document page and has the same originalRenderPage within the class component.
I have tried converting the _document.js component into a functional component rather than a class, but it does not work.
The contents of the _document.js page (directly from the Material UI/Next.js example above) are:
import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from "#material-ui/core/styles";
import theme from "../components/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} />
</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()],
};
};
Related
So I've been stuck trying to find a way to toggle my favicon based on my application theme logic and current theme state stored in localStorage.
I'm currently using a CSS variables approach, with data attributes applied to html body tag to manage my theming. This is all working as desired. I have used a script injected into my html body tag via a custom _document.js file to check if there is a theme preference in my local storage object on the server side prior to first client side render and prevent theme flickering etc.
Where I have run into problems, is trying to extract the logic from my themeToggle component into a custom hook so I can consume this data in my favicon component. When I tried to pull this logic into a useThemeMode hook - I was having issues with document not being available inside my hook I tried writing.
I originally tried to manage this with inline styles in an svg/xml file, but I could not get Next to recognise the inline styles in the SVG correctly - so my intended next step was to create "light" and "dark" versions of my favicon files - both svg and ico and use a template literal in my href to either switch to light or dark file names based on the theme preference currently stored in localStorage object.
I'm relatively new to react / nextjs and dev in general, so I'm sure there are some methods I've overlooked, and I feel sharing this logic with a custom hook to consume in both my favicon and themeToggle components should be relatively straight forward, but I can't seem to grasp it :( - this is what I have so far. Any help to get my head around how to do this effectively would be hugely appreciated. This is my first question posted, so if this is not clear I am sorry, any feedback on how to ask this kind of thing in the future would also be taken onboard.
ThemeToggle component:-
import { useState, useEffect } from "react";
import styled from "styled-components";
import MoonIcon from "./icons/moonIcon";
import SunIcon from "./icons/sunIcon";
const ThemeToggle = () => {
const [activeTheme, setActiveTheme] = useState(document.body.dataset.theme);
const inactiveTheme = activeTheme === "light" ? "dark" : "light";
useEffect(() => {
document.body.dataset.theme = activeTheme;
window.localStorage.setItem("theme", activeTheme);
}, [activeTheme]);
const toggleClickHandler = () => {
setActiveTheme(inactiveTheme);
}
return (
<ToggleButton
type="button"
aria-label={`Change to ${inactiveTheme} mode`}
title={`Change to ${inactiveTheme} mode`}
onClick={() => toggleClickHandler()}
>
{activeTheme === "dark" ? <MoonIcon /> : <SunIcon />}
</ToggleButton>
);
};
export default ThemeToggle;
Script I'm injecting into _document.js via dangerouslySetInnerHTML
const setInitialTheme = `
function getUserPreference() {
if(window.localStorage.getItem('theme')) {
return window.localStorage.getItem('theme')
}
return window.matchMedia('(prefers-color-scheme: light)').matches
? 'light'
: 'dark'
}
document.body.dataset.theme = getUserPreference();
`;
Favicon component where I would like to consume this logic
const Favicon = () => {
//This is where I would like to consume the hook's logic
return (
<Fragment>
<link rel="icon" href={`/favicon/favicon-${theme}.ico`} sizes="any"/>
<link rel="icon" type="image/svg+xml" href={`/favicon/favicon-${theme}.svg`} />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<link rel="manifest" href="/favicon/site.webmanifest" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<link
rel="mask-icon"
href="/favicon/safari-pinned-tab.svg"
color="#5bbad5"
/>
<meta name="apple-mobile-web-app-title" content="Snippit" />
<meta name="application-name" content="<APP NAME>" />
<meta name="msapplication-TileColor" content="#ffc40d" />
<meta name="theme-color" content="#ffffff" />
</Fragment>
);
};
export default Favicon;
So if anyone ever gets stuck with this, I came up with the following solution through utilising the useContext and useEffect hooks to share my theme state across all required components and make the required changes to my ui theme and my favicon component:-
Theme Context Component
import { useState, createContext, useEffect } from "react";
const ThemeContext = createContext({
activeTheme: "",
inactiveTheme: "",
toggleTheme: () => {},
});
export const ThemeModeProvider = ({ children }) => {
const [activeTheme, setActiveTheme] = useState("light");
const inactiveTheme = activeTheme === "light" ? "dark" : "light";
const toggleTheme = () => {
if (activeTheme === "light") {
setActiveTheme("dark");
} else {
setActiveTheme("light");
}
};
useEffect(() => {
const savedTheme = window.localStorage.getItem("theme");
savedTheme && setActiveTheme(savedTheme);
}, []);
useEffect(() => {
document.body.dataset.theme = activeTheme;
window.localStorage.setItem("theme", activeTheme);
const faviconUpdate = async () => {
const favicon = document.getElementById("favicon");
if (activeTheme === "light") {
favicon.href = "/favicon/favicon-light.svg";
} else {
favicon.href = "/favicon/favicon-dark.svg";
}
};
faviconUpdate();
},[activeTheme]);
return (
<ThemeContext.Provider
value={{
activeTheme,
inactiveTheme,
toggleTheme,
}}
>
{children}
</ThemeContext.Provider>
);
};
export default ThemeContext;
I'm sure there are better solutions, but this gives me the added benefit of being able to consume my theme context anywhere I like in future components after wrapping my app with my ThemeModeProvider.
I came across a useful tutorial on dynamic favicons by Renaissance Engineer
that helped me solve the favicon switching based on the theme context.
I want to add a user behavior tracking in my next.js application. I am using Next 10.2.3. In the _app.js I have a function that will call the tracking to start once the page is loaded. I have the impression that every time the page is loaded the function in _app.js will load(reload) as well but unfortunately at some point it does not. Let me know if these are correct or I miss understood some part. This app is hosted in vercel. how the code looks like below.
import App from "next/app";
import Head from "next/head";
import * as tool from "../lib/tools";
const MyApp = ({ Component, pageProps }) => {
tool.callUserBehavior(); //i want to call this function everytime the page is rendered/loaded
return (
<>
<Head>
<link rel="shortcut icon" href={getStrapiMedia(global.favicon.url)} />
</Head>
{/* Display the content */}
<Layout global={global}>
<Component {...pageProps} />
</Layout>
</>
);
};
MyApp.getInitialProps = async (ctx) => {
// Calls page's `getInitialProps` and fills `appProps.pageProps`
const appProps = await App.getInitialProps(ctx);
// Fetch global site settings from Strapi
const global = await getGlobalData();
// Pass the data to our page via props
return { ...appProps, pageProps: { global, path: ctx.pathname } };
};
export default MyApp;
I am calling a JavaScript SDK and invoking it's function in following fashion in my HTML code and it is working fine as expected.
Here is the code
<!DOCTYPE html>
<head>
<title>My Test App</title>
<script src="scripts/abc.js"></script>
<script src="scripts/xyz.js" onload="initXYZ('Param1')"></script>
</head>
</html>
Now, I want to call this same Javascript SDK from a react web page. I want to call the scripts and invoke the initXYZ('Param1') function.
So far, I am able to load the SDK, but I am not sure how to call the function as i did above. Here is the code I wrote in react app.
import React, {useEffect, useRef} from "react";
import "./App.css"
const App = () => {
const instance = useRef(null);
useEffect(() => {
const settingsTag = document.createElement("script");
settingsTag.src = "scripts/abc.js";
instance.current.appendChild(settingsTag);
const websdkTag = document.createElement("script");
websdkTag.src = "scripts/xyz.js";
instance.current.appendChild(websdkTag);
}, []);
return (
<>
<h1>My React app</h1>
<div ref={instance} >
</>
);
};
export default App;
Can you please help me to understand how to invoke the function in above code. Also is there a better way to what I did here?
This is what I do with my own SDK.
Simply creates as many classes as you have sections in your desired SDK.
Embed all those sub SDKs into an object SDK that you reference from your app.
See how it might look like:
Sdk.js
// From here, pretend the following is the content of your sdk.js
class SdkAbc {
static shared // The singleton
constructor() {
// If already created, returned the singleton
if (SdkAbc.shared) return SdkAbc.shared
// Actually create the instance
SdkAbc.shared = this
// You might want to write your init code right below
}
version() {
return 1.3
}
}
class SdkXyz {
// Same as above
}
// You SDK singleton can host as many SDK sections as you need
const Sdk = {
abc: new SdkAbc(),
xyz: new SdkXyz(),
}
export default Sdk
App.js
import React, {useEffect, useRef} from "react";
import "./App.css"
import sdk from './sdk/sdk' // provided sdk.js is your sdk file located in /sdk folder
const App = () => {
return (
<>
<h1>My React app</h1>
<div>Sdk Abc version: {Sdk.abc.version()} >
</>
);
};
export default App;
you can use react-helmet to add any js file to page
I'm using Gatsby and I would like to create a one site using multilanguage, so far I've defined pages/index.js which contains this:
import React from "react"
import Layout from "../components/layout/layout"
import BGTState from "../context/bgt/bgtState"
import { Router } from "#reach/router"
import Home from "../components/pages/home"
import Collection from "../components/pages/collection"
import NotFound from "../components/pages/404"
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Router>
<Home path="/" />
<Collection path="collection/:id" />
<NotFound default />
</Router>
</Layout>
</BGTState>
)
}
export default IndexPage
and I have modified gatsby-node.js as:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
each request is forwarded on index.js, but there is a problem. I'm using the plugin gatsby-plugin-intl that add to the url a dynamic prefix like: http://localhost:3001/en/
If I visit http://localhost:3001/en/, then I get the NotFound component displayed because no Route match the url. Is there a way to prefix the url and reroute everything to the correct component?
Why you are using client-only routes/wrapping everything inside the <Router>?
I don't know what's the goal in your scenario to change the gatsby-node.js with:
// Implement the Gatsby API onCreatePage. This is
// called after every page is created.
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
if (page.path === "/") {
page.matchPath = "/*"
createPage(page)
}
}
If you are not using client-only routes, you can remove them.
It's a broad question but, just define your languages and translation files. In your gatsby-config.js:
plugins: [
{
resolve: `gatsby-plugin-intl`,
options: {
// language JSON resource path
path: `${__dirname}/src/intl`,
// supported language
languages: [`en`,`es`],
// language file path
defaultLanguage: `en`,
// option to redirect to `/en` when connecting `/`
redirect: true,
},
},
]
The useIntl hook will capture the internal requests so, you just need to worry about the views, forgetting the routing:
import React from "react"
import { useIntl, Link, FormattedMessage } from "gatsby-plugin-intl"
const IndexPage = () => {
const intl = useIntl()
return (
<Layout>
<SEO title={intl.formatMessage({ id: "title" })}/>
<Link to="/page-2/">
<FormattedMessage id="go_page2" />
</Link>
</Layout>
)
}
export default IndexPage
Your Collection component should be a page, wrapped inside /page folder, or a custom collection with a specific id. If that page is dynamically created, you should manage the customizations in your gatsby-node.js, and, in that case, it should be a template of collections in that scenario.
To link between pages, I would recommend using page-queries to get the needed data to create your components. Your page should look like:
const IndexPage = () => {
return (
<BGTState>
<Layout>
<Link to="/"> // home path
<Link to="collection/1">
</Layout>
</BGTState>
)
}
export default IndexPage
The 404-page will automatically be handled by Gatsby, redirecting all wrong requests (in development will show a list of pages). Your other routing should be managed using the built-in <Link> component (extended from #reach/router from React).
To make dynamic the <Link to="collection/1"> link, you should make a page query, as I said, to get the proper link to build a custom dynamic <Link> from your data.
Once you installed the gatsby-plugin-intl plugin, all your pages will be prefixed automatically, however, to point to them using <Link> or navigate you need to get the current language and prefix it:
export const YourComponent = props => {
const { locale } = useIntl(); // here you are getting the current language
return <Link to={`${locale}/your/path`}>Your Link</Link>;
};
Because useIntl() is a custom hook provided by the plugin, the value of locale will be automatically set as you change the language.
I import CSS files from local files and node modules:
//> Global Styling
// Local
import "../styles/globals.scss";
// Icons
import "#fortawesome/fontawesome-free/css/all.min.css";
// Bootstrap
import "bootstrap-css-only/css/bootstrap.min.css";
// Material Design for Bootstrap
import "mdbreact/dist/css/mdb.css";
This works as intended on my local development version. All styles appear as they should be.
As you can see here, the styling is different on local and production.
(Take a look at font and buttons)
(Development left, Production right)
My next.config.js is:
//#region > Imports
const withSass = require("#zeit/next-sass");
const withCSS = require("#zeit/next-css");
const withFonts = require("next-fonts");
const withImages = require("next-images");
const withPlugins = require("next-compose-plugins");
//#endregion
//#region > Exports
module.exports = [
withSass({
webpack(config, options) {
config.module.rules.push({
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
use: {
loader: "url-loader",
options: {
limit: 100000,
},
},
});
return config;
},
}),
withPlugins([withCSS, withFonts, withImages]),
];
//#endregion
/**
* SPDX-License-Identifier: (EUPL-1.2)
* Copyright © 2020 InspireMedia GmbH
*/
It seems the MDB styling is being overwritten by bootstrap on building the app. I deploy my app by using next build && next export && firebase deploy and use the ./out folder for deployment source.
You can find the code here: https://github.com/aichner/nextjs-redux-template
If the issue is incorrect styling. (as you are using material-ui) :
Create _document.js under pages directory.
Fill the file with following code :
import React from "react";
import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheets } from "#material-ui/styles"; // works with #material-ui/core/styles, if you prefer to use it.
import theme from "../Theme"; // change this theme path as per your project
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{/* Not exactly required, but this is the PWA primary color */}
<meta name="theme-color" content={theme.palette.primary.main} />
</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(),
],
};
};
Reason : Material UI uses context behind the scenes to apply its styling. Due to NextJs server side rendering, this context will be lost. So, we need to tell Next to make use of that previous context. The above code does that.