How to use Gatsby to display a folder of images and markdown files - node.js

So I'm very new to Gatsby, react, GraphQL, etc. In the past I've used pure CSS, HTML, and javascript to make my sites. Although, I was interested in Gatsby and the capabilities of it, so I decided to challenge myself and learn it.
I'm putting together a portfolio site for myself and for ease of updating, I would like to be able to add new projects through creating new folders, running a build script, and dropping the built site into my FTP.
This is how my folder structure for projects is set up:
-src
--projects
---1-daido-moriyama
----1-dm-frontcover.jpg
----2-dm-spread.jpg
----3-dm-backcover.jpg
----project-metadata.md
[...]
---2-lunch-from-a-care-package
----1-lf-wordmark.png
----2-lf-logo.png
----3-lf-poster.jpg
----project-metadata.md
[...]
The site is a single page, so no need to create new pages for each project. I just have them sorted into numbered folders because that would be the easiest to update for myself.
Ideally I would want to take the title and description from each project's markdown file, and put the title in an h3, the description in a p, and then display the images in a div, which when styled will become a carousel.
Mockup of the design
My current progress
I've been running some tests and have been able to access the markdown files using allMarkdownRemark, and the images using allImageSharp. It was hacky, but it worked, the only problem is that it was displaying all of the images, and not just the images needed for each project. Say, I have 8 images in a project, and 5 in another, it would display all 13 images.
Is there a way to do what I'm trying to do with Gatsby? Or should I just give up and move back to Jekyll…
gatsby-config.js:
module.exports = {
siteMetadata: {
title: 'J.C.R.'
},
plugins: [
'gatsby-plugin-react-helmet',
'gatsby-plugin-sass',
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'projects',
path: `${__dirname}/src/projects/`
}
},
'gatsby-transformer-remark',
'gatsby-transformer-sharp',
'gatsby-plugin-sharp',
`#dream-bit-de/gatsby-plugin-better-page-tree`
]
}
gatsby-node.js:
const path = require('path')
module.exports.onCreateNode = ({ node, actions}) => {
const {createNodeField} = actions
if (node.internal.type === 'MarkdownRemark') {
const slug = path.basename(path.dirname(node.fileAbsolutePath, '.md'))
createNodeField({
node,
name: 'slug',
value: slug
})
}
}
Work component:
import React from 'react'
import { graphql, useStaticQuery } from 'gatsby'
import Img from 'gatsby-image'
const Work = () => {
const data = useStaticQuery(graphql`
query {
allMarkdownRemark(
sort: { order: ASC, fields: [frontmatter___position]}
) {
edges {
node {
frontmatter {
title
description
}
fields {
slug
}
}
}
}
allFile (
filter: {
ext: {eq: ".jpg"}
},
sort: {
order: ASC,
fields: [relativePath]
}
) {
edges {
node {
relativePath
relativeDirectory
name
ext
id
base
}
}
}
}
`)
console.log(data)
return (
<div id="work">
<ol>
{data.allMarkdownRemark.edges.map((edge) => {
return (
<li class={edge.node.fields.slug}>
<h3>{edge.node.frontmatter.title}</h3>
<p>{edge.node.frontmatter.description}</p>
{data.allFile.edges.map((edge) => {
return (
<img src={`../projects${edge.node.relativeDirectory}/${edge.node.name}-${edge.node.base}${edge.node.ext}`}></img>
)
})}
</li>
)
})}
</ol>
</div>
)
}
export default Work
index.js:
import React from 'react'
import Head from '../components/head'
import Info from '../components/info'
import Work from '../components/work'
import '../styles/index.scss'
const indexPage = () => {
return (
<div>
<Head/>
<Info/>
<Work/>
</div>
)
}
export default indexPage

I should dive deeper in the project to fully understand how you could handle that directly with the graphql query, but a short fix would be to filter out the images not related to the project.
{data.allFile.edge0s.filter((item) => item.name.includes(edge.node.frontmatter.id).map((edge) => {
return (
<img src={`../projects${edge.node.relativeDirectory}/${edge.node.name}-${edge.node.base}${edge.node.ext}`}></img>
)
})}
You will need to add a specific id on the name of your files related to the project so when you fetch them you filter out the one not related.
To be better, you could maybe format your imageSchema and add a specific property to handle the case so you won't need to format the image name and then instead of .includes() you could do item.myProperty === myCustomSchemaProperty.

It's much easier than you've tried so far. I would recommend using gatsby-image since all the images belong to the Gatsby ecosystem when you add the following:
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'projects',
path: `${__dirname}/src/projects/`
}
},
Gatsby internally will parse everything inside /src/projects/ and will create nodes to make the schema available for GraphQL, so:
{
allFile(filter: {extension: {eq: "jpg"}}) {
edges {
node {
childImageSharp{
fluid{
...GatsbyImageSharpFluid
}
}
}
}
}
}
Then in your component just:
{data.allFile.edges.map((edge) => {
return <Img fluid={edge.childImageSharp.fluid} />
})}

Related

React data router - show fallback for loader

I'm using react-router v 6.4 with createBrowserRouter to support the new data API.
I have routes that have a loader, and this loader can take 1-2 sec to get the data from the server, and I want to show a loading animation at that time.
See the following as a simple example of what I have, and a comment pointing to what I was expecting to do/find in the docs:
const router = createBrowserRouter([
{
path: '/',
element: <Layout/>,
children: [
{
index: true,
element: <Screen title="Home"/>,
},
{
path: 'materials',
loader: async () => {
return (await fetch('/api/materials')).json()
},
fallbackElement: <Loading />, // <<--- THIS IS WHAT I WAS EXPECTING TO DO
element: <Materials/>,
},
{
path: 'projects',
loader: async () => {
return (await fetch('/api/projects')).json()
},
element: <Projects/>,
},
],
},
])
Could not find how to place a "fallback" element on a route to show while the loader is waiting for the data, only to place a fallbackElement on the RouterProvider component, but that is not what I want (it shows the fallback element only on the mount of RouterProvider, not when changing between routes).
Seems kinda weird that such a thing is not supported, and cannot really find answers through the search here as well.
As per the documentation, on the component consuming the loader data you have to use React.Suspense and Await components to show the fallback, something like this:
import { Await, useLoaderData } from "react-router-dom";
function Book() {
const { book, reviews } = useLoaderData();
return (
<div>
<h1>{book.title}</h1>
<p>{book.description}</p>
<React.Suspense fallback={<ReviewsSkeleton />}>
<Await
resolve={reviews}
errorElement={
<div>Could not load reviews 😬</div>
}
children={(resolvedReviews) => (
<Reviews items={resolvedReviews} />
)}
/>
</React.Suspense>
</div>
);
}
https://reactrouter.com/en/main/components/await#await
That's in theory, because I've done that and my loaders are not showing either.

Can't get html element using js file in SPFX

I am trying to build dynamic content from a SharePoint list using SPFX. I'd like to use jQuery to build an accordion view of the data. The issue is that I can't even seem to get the element once the page is rendered.
In my code I am requiring a file called ota.js with the following code:
console.log('Start');
function otaExpand(){
console.log('otaExpand Function Called');
let spListContainer = document.getElementById('spListContainer');
console.log(spListContainer);
}
window.addEventListener("load", otaExpand());
In my ts file this is my render method:
public render(): void {
this.domElement.innerHTML = `
<div>
<div id="spListContainer">TEST</div>
</div>
`;
//this._renderListAsync();
//($('.accordion', this.domElement) as any).accordion();
}
When I review the console, I get my messages, but the element itself comes back as null.
console.log
I am using SharePoint 2019 on premise with the following configuration.
+-- #microsoft/generator-sharepoint#1.10.0
+-- gulp-cli#2.3.0
`-- yo#2.0.6
node --version
v8.17.0
I should also mention I am using TypeScript with no JavaScript framework.
Does anyone know why I can't access this element from my js file?
Thanks!
My overall goal is to call list data and apply an accordion style to it (https://jqueryui.com/accordion), but I can't even get passed capturing the element to change it.
I've tried calling my code from a js file as well as trying to put the code directly in the html. Neither worked.
OK, I finally figured out what I was doing wrong. I was calling my jQuery in the render() method rather than in _renderList where this.domElement actually makes sense.
Here's my code in case anyone wants to avoid the pain I put myself through. This allows you to specify a list in the site and you just need to add the fields you want to display.
import { Version } from '#microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneChoiceGroupOption,
IPropertyPaneConfiguration,
PropertyPaneChoiceGroup,
PropertyPaneCustomField,
PropertyPaneTextField
} from '#microsoft/sp-webpart-base';
import { escape } from '#microsoft/sp-lodash-subset';
import styles from './GetSpListItemsWebPart.module.scss';
import * as strings from 'GetSpListItemsWebPartStrings';
import {
SPHttpClient,
SPHttpClientResponse
} from '#microsoft/sp-http';
import * as jQuery from 'jquery';
import 'jqueryui';
import { SPComponentLoader } from '#microsoft/sp-loader';
import PropertyPane from '#microsoft/sp-webpart-base/lib/propertyPane/propertyPane/PropertyPane';
export interface IGetSpListItemsWebPartProps {
title: string;
description: string;
listField: string;
}
export interface ISPLists {
value: ISPList[];
}
export interface ISPList {
ID: string;
Title: string;
Website: {
Description : string,
Url : string
};
Description : string;
}
export default class GetSpListItemsWebPart extends BaseClientSideWebPart<IGetSpListItemsWebPartProps> {
private _getListData(): Promise<ISPLists> {
return this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + "/_api/web/lists/GetByTitle('" + this.properties.listField + "')/Items",SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
});
}
private _renderListAsync(): void {
this._getListData()
.then((response) => {
this._renderList(response.value);
})
.catch(() => {});
}
private _renderList(items: ISPList[]): void {
let listData = `
<h1>${this.properties.title}</h1>
<h2>${this.properties.description}</h2>
<div class="accordion">
`;
items.forEach((item: ISPList) => {
let Description : string;
item.Description ? Description = item.Description : Description = "";
listData += `
<h3> ${item.Title}</h3>
<div>
<table>
<tr>
<td>OTA URL</td>
<td>${item.Website.Description}</td>
</tr>
<tr>
<td>Description</td>
<td>${Description}</td>
</tr>
</table>
</div>
`;
});
listData += '</div>';
this.domElement.innerHTML = listData;
const accordionOptions: JQueryUI.AccordionOptions = {
animate: true,
collapsible: true,
icons: {
header: 'ui-icon-circle-arrow-e',
activeHeader: 'ui-icon-circle-arrow-s'
}
};
jQuery('.accordion', this.domElement).accordion(accordionOptions);
}
public render(): void {
this._renderListAsync();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('title',{
label: strings.TitleFieldLabel
}),
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneTextField('listField', {
label: strings.ListFieldLabel
})
]
}
]
}
]
};
}
public constructor() {
super();
SPComponentLoader.loadCss('//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css');
}
}
Your code from the "ota.js" file is probably called before your HTML is initialized (i.e. before the "render()" function is executed). To make sure this is the case, you could add log to the "render()" function to see when it's called.
In other words, "window.load" event happens long before "render()" function is called. This is how web parts are loaded - dynamically after full load of the page. Or "window.load" does not happen at all - web parts may be loaded by the user when using the page designer, i.e. without page reload.
To fix the issue, you should get the element after it's created, i.e. after the "render()" function creates the element you are trying to get.

Cannot read property 'allContentfulBlogPost' of undefined" after moving query from index.js to component in GatsbyJS (with Contenful and GraphQL)

Moving a query from index.js to midsection.js (a component) gives Cannot read property of undefined.
I made a website with GatsbyJS which gets it's content from Contentful. I accomplished this by following the Build a blazing fast website with GatsbyJS and Contentful tutorial: https://www.youtube.com/watch?v=wlIdop5Yv_Y
In the tutorial you learn the basics of making a query which shows your content from Contentful on the homepage.
Because I like to use Bulma and I'm pretty new to GatsbyJS (new to React as well) I decided to download the Gatsby-Bulma-Quickstart (https://www.gatsbyjs.org/starters/amandeepmittal/gatsby-bulma-quickstart) and compare it to my own website and use what I need.
I decided to use the component structure used in the Quickstart and wanted to move the query for getting my content from the index.js to the midsection.js.
I got everything working until I moved the query.
My index.js looked like this:
import React from 'react'
import { Link } from 'gatsby'
// import Layout from '../components/layout';
const BlogPost = ({node}) => {
return (
<li>
<Link to={node.slug}><h3>{node.title}</h3></Link>
<img src={node.heroImage.resize.src} />
<div>{node.description.childMarkdownRemark.excerpt}</div>
</li>
)
}
const IndexPage = ({data}) => (
<ul className='blog-post'>
{data.allContentfulBlogPost.edges.map((edge) => <BlogPost node={edge.node} />)}
</ul>
)
// const IndexPage = () => <Layout />;
export default IndexPage
export const pageQuery = graphql`
query pageQuery {
allContentfulBlogPost (filter: {
node_locale: {eq: "en-US"}
},
sort:{ fields: [publishDate], order: DESC }
) {
edges {
node {
title
slug
description {
childMarkdownRemark {
excerpt
}
}
heroImage {
resize(width: 300, height: 300) {
src
}
}
}
}
}
}
`
Note: This works, this shows my content. (But as you can see the components etc from the Quickstart are not included (yet))
This is what my index.js looks like right now:
import React from 'react'
import Layout from '../components/layout';
const IndexPage = () => <Layout />;
export default IndexPage
And this is what my midsection.js looks like right now:
import React from 'react'
import { Link } from 'gatsby'
import './style.scss'
const BlogPost = ({node}) => {
return (
<li>
<Link to={node.slug}><h3>{node.title}</h3></Link>
<img src={node.heroImage.resize.src} />
<div>{node.description.childMarkdownRemark.excerpt}</div>
</li>
)
}
const Midsection = ({data}) => (
<ul className="blog-post">
{data.allContentfulBlogPost.edges.map((edge) => <BlogPost node={edge.node} />)}
</ul>
)
export default Midsection
export const pageQuery = graphql`
query pageQuery {
allContentfulBlogPost (filter: {
node_locale: {eq: "en-US"}
},
sort:{ fields: [publishDate], order: DESC }
) {
edges {
node {
title
slug
description {
childMarkdownRemark {
excerpt
}
}
heroImage {
resize(width: 300, height: 300) {
src
}
}
}
}
}
}
`
Using this way of moving the query to a component gives this error in the browser:
TypeError: Cannot read property 'allContentfulBlogPost' of undefined
I'd expected to use the midsection.js component for columns to show available "blog posts" from Contentful. Instead this only works straight from index.js.
Is there some way the query is not working because I moved it from the root folder to the components folder? And if so, what do I need to do to get the result I want?
With an colleague helping me, we found an solution by following these steps:
Change layout.js to:
import './style.scss'
const Layout = ({ children }) => children
export default Layout
Change index.js to:
import React from 'react'
import Layout from '../components/layout';
import Helmet from '../components/helmet';
import Header from '../components/header';
import Midsection from '../components/midsection';
import Footer from '../components/footer';
const IndexPage = ({data}) => (
<Layout>
<Helmet />
<Header />
<Midsection posts={data.allContentfulBlogPost.edges}/>
<Footer />
</Layout>
)
export default IndexPage
export const pageQuery = graphql`
query pageQuery {
allContentfulBlogPost (filter: {
node_locale: {eq: "en-US"}
},
sort:{ fields: [publishDate], order: DESC }
) {
edges {
node {
title
slug
description {
childMarkdownRemark {
excerpt
}
}
heroImage {
resize(width: 300, height: 300) {
src
}
}
}
}
}
}
`
Change midsection.js to:
import React from 'react'
import Link from 'gatsby-link'
import './style.scss'
const BlogPost = ({node}) => {
return (
<li>
<Link to={node.slug}><h3>{node.title}</h3></Link>
<img src={node.heroImage.resize.src} />
<div>{node.description.childMarkdownRemark.excerpt}</div>
</li>
)
}
const Midsection = ({ posts }) => (
<ul className="blog-post">
{posts.map(post => (
<BlogPost key={post.node.slug} node={post.node} />
))}
</ul>
)
export default Midsection
So what was the problem and what solved it?
The query used in this situation is a pageQuery which means that it only works from pages found in the pages folder. If you want to use the data in a component you have to pass it through :)

gatsby image svg not found

When trying to load an SVG image this way:
export const query = graphql`
query {
fileName: file(relativePath: { eq: "logo_large.svg" }) {
childImageSharp {
fluid(maxWidth: 1060) {
...GatsbyImageSharpFluid_withWebp_tracedSVG
}
}
}
}
`
I get TypeError: Cannot read property 'childImageSharp' of null
If I try the exact same but with a .jpg or .png image, it works, so the relative path must be correct.
Any thing I should have in special consideration with SVG's?
"SVG are not supported by this plugin for obvious reasons, they are
vectorial and automatically adjust their size without the need of
plugin like this one"
Correct. If you want to handle multiple types like png + jpg + svg you have to dynamically handle it with gatsby-image or not. You solve this by adding extension and publicURL in your GraphQL query:
...
image {
childImageSharp {
fluid(maxWidth: 500, quality: 92) {
...GatsbyImageSharpFluid
}
}
extension
publicURL
}
...
Add this to your image component:
// svg support
if (!childImageSharp && extension === 'svg') {
return <img style={imageStyle} src={publicURL} alt={alt} />
}
Credit goes to andresmrm on GitHub.
SVG are not supported by this plugin for obvious reasons, they are vectorial and automatically adjust their size without the need of plugin like this one
I know this is an old question with accepted answer but I thought I might add another solution for others' benefit. I found this solution at: https://github.com/gatsbyjs/gatsby/issues/10297
import yourSVG from './logo_large.svg'
const Home = () => <><img src={yourSVG} /></>
Don't know if this helps, but If you are looking for a generic and dynamic Image component that takes a custom file name and acts accordingly I've put together this:
This approach considers that you have an images folder inside your src folder where you have all your images.
const Image = ({ filename, alt }) => (
<StaticQuery
query={graphql`
query {
images: allFile {
edges {
node {
relativePath
name
childImageSharp {
fluid(maxWidth: 600) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
`}
render={data => {
// Handles SVG extension
const extension = filename.match(/[^\\]*\.(\w+)$/)[1]
if (extension === "svg") {
return <img src={require(`../images/${filename}`)} alt={alt}/>
}
// Finds your image among all
const image = data.images.edges.find(n => {
return n.node.relativePath.includes(filename)
})
if (!image) {
return null
}
return (
<Img alt={alt} fluid={image.node.childImageSharp.fluid}/>
)
}}
/>
)
Be careful that this is not optimized and queries for all of your images. If you are trying to optimize performance in every way possible this isn't the best way to go.

React + Material-UI - Warning: Prop className did not match

I'm having difficulty with differences between client-side and server-side rendering of styles in Material-UI components due to classNames being assigned differently.
The classNames are assigned correctly on first loading the page, but after refreshing the page, the classNames no longer match so the component loses its styling. This is the error message I am receiving on the Console:
Warning: Prop className did not match.
Server: "MuiFormControl-root-3 MuiFormControl-marginNormal-4
SearchBar-textField-31"
Client: "MuiFormControl-root-3 MuiFormControl-marginNormal-4
SearchBar-textField-2"
I've followed the Material-UI TextField example docs, and their accompanying Code Sandbox example, but I can't seem to figure out what is causing the difference between the server and client classNames.
I experienced a similar issue when adding Material-UI Chips with a delete 'x' icon. The 'x' icon rendered with a monstrous 1024px width after refreshing. The same underlying issue being that icon was not receiving the correct class for styling.
There are a few questions on Stack Overflow addressing why the client and server might render classNames differently (e.g. need to upgrade to #Material-UI/core version ^1.0.0, using a custom server.js, and using Math.random in setState), but none of these apply in my case.
I don't know enough to tell whether this Github discussion might help, but likely not since they were using a beta version of Material-UI.
Minimal steps to reproduce:
Create project folder and start Node server:
mkdir app
cd app
npm init -y
npm install react react-dom next #material-ui/core
npm run dev
edit package.json:
Add to 'scripts': "dev": "next",
app/pages/index.jsx:
import Head from "next/head"
import CssBaseline from "#material-ui/core/CssBaseline"
import SearchBar from "../components/SearchBar"
const Index = () => (
<React.Fragment>
<Head>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charSet="utf-8" />
</Head>
<CssBaseline />
<SearchBar />
</React.Fragment>
)
export default Index
app/components/SearchBar.jsx:
import PropTypes from "prop-types"
import { withStyles } from "#material-ui/core/styles"
import TextField from "#material-ui/core/TextField"
const styles = (theme) => ({
container: {
display: "flex",
flexWrap: "wrap",
},
textField: {
margin: theme.spacing.unit / 2,
width: 200,
border: "2px solid red",
},
})
class SearchBar extends React.Component {
constructor(props) {
super(props)
this.state = { value: "" }
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange(event) {
this.setState({ value: event.target.value })
}
handleSubmit(event) {
event.preventDefault()
}
render() {
const { classes } = this.props
return (
<form
className={classes.container}
noValidate
autoComplete="off"
onSubmit={this.handleSubmit}
>
<TextField
id="search"
label="Search"
type="search"
placeholder="Search..."
className={classes.textField}
value={this.state.value}
onChange={this.handleChange}
margin="normal"
/>
</form>
)
}
}
SearchBar.propTypes = {
classes: PropTypes.object.isRequired,
}
export default withStyles(styles)(SearchBar)
Visit page in browser localhost:3000 and see this:
red border around TextField component
Refresh the browser and see this:
TextField component's styles are gone
Notice that the red border around TextField disappears.
Relevant Libs:
"react": 16.4.0
"react-dom": 16.4.0
"next": 6.0.3
"#material-ui/core": 1.2.0
The problem is the SSR rendering in Next.js, which produces the style fragment before the page is rendered.
Using Material UI and Next.js (as the author is using), adding a file called _document.js solved the problem.
Adjusted _document.js (as suggested here):
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 '../src/theme'; // Adjust here as well
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()],
};
};
This problem is related to MUI using dynamic class name which contain an ID. The IDs from the server side rendered CSS are not the same as the client side CSS, hence the mismatch error. A good start is to read the MUI SSR documentation
If you have this problem with nextjs (as I did) follow the example provided by the MUI team, which can be found here: material-ui/examples/nextjs
The most important part is in "examples/nextjs/pages/_app.js":
componentDidMount() {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}
the related ticket can be found here: mui-org/material-ui/issues/15073
what it does, is remove the server side rendered stylesheet and replace it by a new client side rendered one
The issue is the server side generates the class names but style sheets are not automatically included in the HTML. You need to explicitly extract the CSS and append it to the UI for the server side rendered components. The whole process is explained here: https://material-ui.com/guides/server-rendering/
There is one other important, separate issue here: Material UI V4 is not React Strict Mode compatible. Strict mode compatibility is slated for version 5 with the adoption of the Emotion style engine.
Until then, be sure you disable React Strict Mode. If you're using Next.js, this is turned on by default if you've created your app using create-next-app.
// next.config.js
module.exports = {
reactStrictMode: false, // or remove this line completely
}
I had the same problem with Next.js and styled component, with the transpilation by Babel. Actually, the class names are different on the client and the server side.
Fix it in writing this in your .babelrc :
{
"presets": ["next/babel"],
"plugins": [
[
"styled-components",
{ "ssr": true, "displayName": true, "preprocess": false }
]
]
}
I met this problem on Material-ui V5. The solution to fix this problem is to make sure that class name generator needs to behave identically on the server and on the client.
so adding the code below in your _app.js:
import { StylesProvider, createGenerateClassName } from '#mui/styles';
const generateClassName = createGenerateClassName({
productionPrefix: 'c',
});
export default function MyApp(props) {
return <StylesProvider generateClassName={generateClassName}>...</StylesProvider>;
}
// 1 . Warning: prop classname did not match. Material ui with React Next.js
// 2 . Use your customization css here
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
title: {
flexGrow: 1,
},
my_examle_classssss: {
with: "100%"
}
}));
// 3 . Here my Component
const My_Example_Function = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Container>
<Examle_Component> {/* !!! Examle_Component --> MuiExamle_Component*/}
</Examle_Component>
</Container>
</div>
);
}
export default My_Example_Function
// 4. Add name parameter to the makeStyles function
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
title: {
flexGrow: 1,
},
my_examle_classssss: {
with: "100%"
},
}), { name: "MuiExamle_ComponentiAppBar" });
{/* this is the parameter you need to add { name: "MuiExamle_ComponentiAppBar" } */ }
{/* The problem will probably be resolved if the name parameter matches the first className in the Warning: you recive..
EXAMPLE :
Warning: Prop `className` did not match.
Server: "MuiSvgIcon-root makeStyles-root-98"
Client: "MuiSvgIcon-root makeStyles-root-1"
The name parameter will be like this { name: "MuiSvgIcon" }
*/ }
I like to share this mismatching case:
next-dev.js?3515:32 Warning: Prop className did not match. Server:
"MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-12
MuiSwitch-switchBase MuiSwitch-colorSecondary" Client:
"MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-12
MuiSwitch-switchBase MuiSwitch-colorSecondary
PrivateSwitchBase-checked-13 Mui-checked"
On client there are two more classes which means that the behavior on client-side is different. In this case, this component shouldn't render on server-side. The solution is to dynamically render this component:
export default dynamic(() => Promise.resolve(TheComponent), { ssr: false });
I had a problem with different classNames for client and server. I was using React, Material-UI, makeStyles and SSR (server-side rendering).
The error was:
Warning: Prop `className` did not match. Server: "jss3" Client: "App-colNav-3"
I spent several hours before I figured out that I had discrepancy in webpack mode for client and server. The scripts in package.json were:
"devServer": "webpack --config webpack.server.config.js --mode=production --watch",
"devClient": "webpack --mode=development --watch",
After I changed both to have development mode, the problem was solved :)
"devServer": "webpack --config webpack.server.config.js --mode=development --watch",
"devClient": "webpack --mode=development --watch",
If somebody is still struggling even after trying above solutions, Try this
If you have used noSsr prop in any of your components or theme, then remove it.
I had the following config in mui theme object, which was causing this problem.
import { createTheme, responsiveFontSizes } from "#mui/material/styles";
let theme = createTheme({
components: {
MuiUseMediaQuery: {
defaultProps: {
noSsr: true,
},
},
},
palette: {
mode: "light",
common: {
black: "#000",
white: "#fff",
},
primary: {
main: "#131921",
contrastText: "#fff",
},
secondary: {
main: "#fb6a02",
contrastText: "#fff",
}
}
})
RemovingnoSSr fixed all of the issues in my app including style mismatch between client and server.
The problem is cause by Nextjs server side rendering. In order to solve I do as following:
Make a component to detect whether is it from Client side
import { useState, useEffect } from "react";
interface ClientOnlyProps {}
// #ts-ignore
const ClientOnly = ({ children }) => {
const [mounted, setMounted] = useState<boolean>(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted ? children : null;
};
export default ClientOnly;
Wrap my page component using ClientOnly component
export default function App() {
return (
<ClientOnly>
<MyOwnPageComponent>
</ClientOnly>
);
}
So the idea is, if it is client side then only render the component on the page. Therefore if current rendering is from Client side, render <MyOwnPageComponent>, else render nothing
In my case the issue happened because of different compilation modes of webpack for client-side code and server-side: client's bundle was generated by webpack using "production" mode, while server ran some SSR code from a package optimized for "development". This created a different "className" hash in styled-components in generateAndInjectStyles():
if (process.env.NODE_ENV !== 'production') dynamicHash = phash(dynamicHash, partRule + i);
So my fix was just to align the webpack modes.
You can add the name in anywhere you use makeStyles, like this:
const useStyles = makeStyles({
card: {
backgroundColor: "#f7f7f7",
width: "33%",
},
title: {
color: "#0ab5db",
fontWeight: "bold",
},
description: {
fontSize: "1em"
}
}, { name: "MuiExample_Component" });
I am not sure how it works, but I found it here: Warning: Prop `className` did not match ~ Material UI css arbitrarily breaks on reload
I'm also using NextJS + MUI v5 and I ran into this exact error right after merging Git branches. I suspect the merge corrupted something in the cache. I deleted the contents of .next/ and restarted the dev server and the error went away.
#Leonel Sanches da Silva's answer didn't work for me, as #material-ui/styles is deprecated, but using a snippet I found for another (non-material UI) project seems to have worked just fine for me:
Hat tip to Raul Sanchez on dev.to for the answer to this one.
Next doesn't fetch styled-components styles on the server, to do that you need to add this page to pages/_document.js:
import Document from 'next/document'
import { ServerStyleSheet } from 'styled-components'
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
}
} finally {
sheet.seal()
}
}
}
This code may update, so check Next's styled-components example for the latest.

Resources