Set theme on server before sending to browser - reactjs

I'm using Nextjs and Material UI. I want to change the theme based on the hostname(different clients will have different hostnames and different themes).
I did something like dynamically import theme file inside useEffect, but that briefly shows the default theme and then changes the colours after import is done.
I want to render the html with the respective theme for the client on the server itself. What I've done below works(I saw the network tab preview of html with respective theme applied), but don't know how it works since I am setting theme in useState(createTheme(themeObj))
This is theme/index.ts:
import defaultTheme from './default.theme';
import org1Theme from './org1.theme';
const allThemes = {
org1: org1Theme,
default: defaultTheme,
};
export default allThemes;
In _app.ts I import allThemes and use getInitialProps to get the theme object from allThemes based on hostname and send it to MyApp component as props.
_app.tsx:
MyApp.getInitialProps = async (appContext: AppContext) => {
const [hostName] = appContext.ctx.req?.headers.host?.split(':') || [''];
let themeKey = 'default';
if (hostName === 'org1.com') themeKey = 'org1';
return {
...appProps,
themeObj: allThemes[themeKey],
};
};
The MyApp component in _app.tsx:
import allThemes from '../theme';
function MyApp({themeObj}: AppProps) {
const [theme] = useState(createTheme(themeObj)); //themeObj from props from getInitialProps
return (
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
...
My question is, is this the right way to do what I'm trying to achieve and don't understand how useState(createTheme(themeObj)) works(I saw the network tab preview with respective theme applied) since useState is executed on client side(right?). I'm new to nextjs(react too, kind of) and don't know if something might break because I don't fully understand client/server side rendering.

Related

Using React.Context with Nextjs13 server-side components

Next13 was released a week ago, and I am trying to migrate a next12 app to a next13.
I want to use server-side components as much as possible, but I can't seem to use
import { createContext } from 'react';
in any server component.
I am getting this error:
Server Error
Error:
You're importing a component that needs createContext. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default.
,----
1 | import { createContext } from 'react';
: ^^^^^^^^^^^^^
`----
Maybe one of these should be marked as a client entry with "use client":
Is there an alternative here or do I have to resort to prop drilling to get server-side rendering?
It seems like I can use createServerContext
import { createServerContext } from 'react';
If you're using Typescript and React 18, you'll also need to add "types": ["react/next"] to your tsconfig.json compiler options, since this is a not-yet-stable function.
This is a new feature from React's SSR to recognize whether a component is client-side or server-side. In your case, createContext is only available on the client side.
If you only use this component for client-side, you can define 'use client'; on top of the component.
'use client';
import { createContext } from 'react';
You can check this Next.js document and this React RFC for the details
According to Next.js 13 beta documentation, you cannot use context in Server Components:
In Next.js 13, context is fully supported within Client Components, but it cannot be created or consumed directly within Server Components. This is because Server Components have no React state (since they're not interactive), and context is primarily used for rerendering interactive components deep in the tree after some React state has been updated
However, there are alternative ways to handle data in the new approach, depending on your case. F.e. if you fetched the data from the server in a parent component and then passed it down the tree through Context, you can now fetch the data directly in all the components that depend on this data. React 18 will dedupe (de-duplicate) the fetches, so there are no unnecessary requests.
There are more alternatives in the documentation.
I've made a tiny package to handle context in server components, works with latest next.js, it's called server-only-context:
https://www.npmjs.com/package/server-only-context
Usage:
import serverContext from 'server-only-context';
export const [getLocale, setLocale] = serverContext('en')
export const [getUserId, setUserId] = serverContext('')
import { setLocale, setUserId } from '#/context'
export default function UserPage({ params: { locale, userId } }) {
setLocale(locale)
setUserId(userId)
return <MyComponent/>
}
import { getLocale, getUserId } from '#/context'
export default function MyComponent() {
const locale = getLocale()
const userId = getUserId()
return (
<div>
Hello {userId}! Locale is {locale}.
</div>
)
}
This is the code for it, it's really simple:
import 'server-only'
import { cache } from 'react'
export default <T>(defaultValue: T): [() => T, (v: T) => void] => {
const getRef = cache(() => ({ current: defaultValue }))
const getValue = (): T => getRef().current
const setValue = (value: T) => {
getRef().current = value
}
return [getValue, setValue]
}

Material UI breaks NextJS app in production

I created a NextJS app which uses server-side rendering and Material UI. It works fine in development.
The app compiles and builds without errors when I run "next build". When I run it with NODE_ENV=production, the webpage renders just fine but many features no longer work. For example:
The "Hidden" component for Material UI never shows any of its sub-components nested within even when it should (in my development app, it hides and shows certain divs depending on screen size).
None of the buttons on the webpage work. All these buttons have "onClick" events whose callback functions modify the React state object in some way when clicked. However, nothing happens when these are clicked. The state remains the same, so I'm assuming these functions never get called when these click events occur. This is true for Material UI's Button components as well as plain old HTML buttons (as JSX).
Everything works completely fine when I run this in dev mode on my laptop. However, when I build the NextJS app and deploy it to the server in production mode, I encounter the problems listed above. So far, my research has only turned up the possibility of class name conflicts during builds (this was said on Material UI's FAQ page). Has anyone had the same problem as I'm having?
EDIT: I just started a barebones NextJS app containing only one index page and minimal dependencies with one state parameter and one button to modify the parameter via an onClick event. I'm having the same problem. The button works in development but not in production. So this would be a NextJS issue rather than a Material UI problem. But that still doesn't explain why the "Hidden" component for Material UI always remains hidden regardless of screen size. Maybe it's both a Next JS and Material UI problem.
I think this can help you.
in _document.js
import React from 'react';
import Document, {
Html, Main, NextScript,
} from 'next/document';
import { ServerStyleSheets } from '#material-ui/core/styles';
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
// `getInitialProps` belongs to `_document` (instead of `_app`),
// it's compatible with server-side generation (SSG).
MyDocument.getInitialProps = async (ctx) => {
// 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()],
};
};
and in add in _app.js
React.useEffect(() => {
// Remove the server-side injected CSS.
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles);
}
}, []);
Hope it work !
About the first Question, Because you open the ssr, The material ui can't judge the breakpointer in server side. Here's an official example。server-sider-rendering. You need to determine the user device based on the UA header to determine the page sizer. Use matches to determine what you need to display
_document.js -> MaterialUI styles for SSR.
import React from 'react'
import NextDocument from 'next/document'
import { ServerStyleSheet as StyledComponentSheets } from 'styled-components'
import { ServerStyleSheets as MaterialUiServerStyleSheets } from '#material-
ui/styles'
export default class Document extends NextDocument {
static async getInitialProps(ctx) {
const styledComponentSheet = new StyledComponentSheets()
const materialUiSheets = new MaterialUiServerStyleSheets()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: App => props =>
styledComponentSheet.collectStyles(
materialUiSheets.collect(<App {...props} />),
),
})
const initialProps = await NextDocument.getInitialProps(ctx)
return {
...initialProps,
styles: [
<React.Fragment key="styles">
{initialProps.styles}
{materialUiSheets.getStyleElement()}
{styledComponentSheet.getStyleElement()}
</React.Fragment>,
],
}
} finally {
styledComponentSheet.seal()
}
}
}

React-JSS with material-ui and server side rendering

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

NEXT JS - How to prevent layout get re-mounted?

Trying next with layout pattern:
https://github.com/zeit/next.js/tree/canary/examples/layout-component
And the problem is that Layout component get remounted on every page change. I need to use layout component as a Container so it'll fetch data from server on every mount. How can I prevent layout to get re-mounted? Or am I missing something there?
This helped me for persistent layouts. The author puts together a function that wraps your page components in your Layout component and then passes that fetch function to your _app.js. This way the _app.js is actually the components that renders the Layout but you get to specify which pages use which layout (in case you have multiple layouts).
So you have the flexibility of having multiple layouts throughout your site but those pages that share the same layout will actually share the same layout component and it will not have to be remounted on navigation.
Here is the link to the full article
Persistent Layout Patterns in Next.js
Here are the important code snippets. A page and then _app.js
// /pages/account-settings/basic-information.js
import SiteLayout from '../../components/SiteLayout'
import AccountSettingsLayout from '../../components/AccountSettingsLayout'
const AccountSettingsBasicInformation = () => (
<div>{/* ... */}</div>
)
AccountSettingsBasicInformation.getLayout = page => (
<SiteLayout>
<AccountSettingsLayout>{page}</AccountSettingsLayout>
</SiteLayout>
)
export default AccountSettingsBasicInformation
// /pages/_app.js
import React from 'react'
import App from 'next/app'
class MyApp extends App {
render() {
const { Component, pageProps, router } = this.props
const getLayout = Component.getLayout || (page => page)
return getLayout(<Component {...pageProps}></Component>)
}
}
export default MyApp
If you put your Layout component inside page component it will be re-remounted on page navigation (page switch).
You can wrap your page component with your Layout component inside _app.js, it should prevent it from re-mounting.
Something like this:
// _app.js
import Layout from '../components/Layout';
class MyApp extends App {
static async getInitialProps(appContext) {
const appProps = await App.getInitialProps(appContext);
return {
...appProps,
};
}
render() {
const { Component, pageProps } = this.props;
return (
<Layout>
<Component {...pageProps} />
<Layout />
);
}
}
export default MyApp;
Also, make sure you replace all the to <Link href=""></Link>, notice that only have change the Html tag to link.
I struggled because with this for many days, although I was doing everything else correctly, these <a> tags were the culprit that was causing the _app.js remount on page change
Even though this is the topic Layout being mounted again and again, the root cause of this problem is that you have some data loaded in some child component which is getting fetched again and again.
After some fooling around, I found none of these problem is actually what Next.Js or SWR solves. The question, back to square one, is how to streamline a single copy of data to some child component.
Context
Use context as a example.
Config.js
import { createContext } from 'react'
export default createContext({})
_App.js
import Config from '../Config'
export default function App({ Component, pageProps }) {
return (
<Config.Provider value={{ user: { name: 'John' }}}>
<Component {...pageProps} />
</Config.Provider>
)
}
Avatar.js
import { useContext } from 'react'
import Config from '../Config'
function Avatar() {
const { user } = useContext(Config)
return (
<span>
{user.name}
</span>
)
}
export default Avatar
No matter how you mount and dismount, you won't end up with re-render, as long as the _app doesn't.
Writable
The above example is only dealing with readable. If it's writable, you can try to pass a state into context. setUser will take care the set in consumer.
<Provider value={useState({})} />
const [user, setUser] = useContext(Config)
setUser is "cached" and won't be updated. So we can use this function to reset the user anytime in child consumer.
There're other ways, ex. React Recoil. But more or less you are dealing with a state management system to send a copy (either value or function) to somewhere else without touching other nodes. I'll leave this as an answer, since even we solved Layout issue, this problem won't disappear. And if we solve this problem, we don't need to deal with Layout at all.

Exposing React child component to JavaScript on webpage

I'm pretty new to rxjs, and trying to understand what's needed here to expose the Chat object in the Bot Framework, as I need to call some methods in it I'll add. I essentially need access to the created Chat component from the webpage, which right now has a BotChat.App. There's also a BotChat.Chat, but that doesn't seem to be the instance I need access to.
The following is used from the Bot Framework by calling BotChat.App({params});
That in turn creates a Chat component (eventually in App.tsx below). I need to basically expose the Chat instance that is used, as I want to modify it.
BotChat.ts (Complete)
export { App, AppProps } from './App';
export { Chat, ChatProps } from './Chat';
export * from 'botframework-directlinejs';
export { queryParams } from './Attachment';
export { SpeechOptions } from './SpeechOptions'
export { Speech } from './SpeechModule'
import { FormatOptions } from './Types';
// below are shims for compatibility with old browsers (IE 10 being the main culprit)
import 'core-js/modules/es6.string.starts-with';
import 'core-js/modules/es6.array.find';
import 'core-js/modules/es6.array.find-index';
And here in App.tsx note the Chat component used below. That is what I need to expose up through the webpage. A bit confused as to if it's exporting a "Chat" type as opposed to getting access to the instance of Chat being used in App.tsx. Hope this makes some sense :)
App.tsx (Complete)
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Chat, ChatProps } from './Chat';
import * as konsole from './Konsole';
export type AppProps = ChatProps;
export const App = (props: AppProps, container: HTMLElement) => {
konsole.log("BotChat.App props", props);
ReactDOM.render(React.createElement(AppContainer, props), container);
}
const AppContainer = (props: AppProps) =>
<div className="wc-app">
<Chat { ...props } /> //<--------------This is what I want to get
//access to on the webpage, which currently
//only uses BotChat.App() to initialize the
//web chat control. Not sure how to expose
//this _instance_ to App.tsx and then expose
//that instance to the webpage.
</div>;
Web Chat has a Redux store and a RxJS stream that you can expose to interact with the <Chat> component. In React, you can, but people usually don't expose any functions out of it. Short reason: the contract of a React component is props, not functions.
For RxJS stream that you can access the chat history, you can look at the backchannel sample. But it's a read-only stream.
For other interactivities, look at Store.ts to see what actions it is using. Then, expose the Redux store at Chat.ts (easy hack: thru window variable, better: make a callback props at <Chat>).
If you need more interactivities that is not in the existing Redux actions, feel free to add some more. For example, this pull request should give you a sense of injecting chat history.

Resources