I have stared investigate MUI lates version and I see that responsive brake points and all other stuff are base screen size.
But we are developing some kind of dashboard as reusable component. And I want to use default Material-ui responsivness, I like how we can in MUI component define override base on breakpoints.
But our Dashboard component and its breakpoints will work just if whole component will be rendered in IFRAME.
Its way in MUI how to solve this problem? Or use somehow container queries?
Finally I thinking about to override MUI theme brake points in container scope base on size of parent container and its size.
Can you point me to solution?
Override of brakepoints could be done like following code but I am afraid about performance.
import React from "react";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import HeaderComponent from "./header";
import "./App.css";
const values = {
xs: 0,
sm: 426,
md: 960,
lg: 1280,
xl: 1920
};
// here I can do some calculation base on element size
const theme = createMuiTheme({
palette: {
primary: {
main: "#000000"
},
secondary: {
main: "#9f9f9f"
}
},
breakpoints: {
keys: ["xs", "sm", "md", "lg", "xl"],
up: (key) => `#media (min-width:${values[key]}px)`
}
});
function Dashboard() {
return (
<MuiThemeProvider theme={theme}> // define cope theme provider
<div>
<HeaderComponent></HeaderComponent>
</div>
</MuiThemeProvider>
);
}
export default App;
Thanks for your help
The best result for me was something like this:
import { useCallback, useState } from 'react';
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export enum SizeTo {
DOWN_TO = 'downTo',
UP = 'up',
}
const getMatches = (el: HTMLElement | null | undefined, size: number, option: SizeTo): boolean => {
// Prevents SSR issues
if (typeof window !== 'undefined' && el) {
if (option === SizeTo.DOWN_TO) {
return el.offsetWidth <= size;
}
return el.offsetWidth > size;
}
return false;
};
function useContainerMediaQuery<T extends HTMLElement = HTMLDivElement>(size: number, option: SizeTo): [
(node: T | null) => void,
boolean,
] {
const [ref, setRef] = useState<T | null>(null);
const [matches, setMatches] = useState<boolean>(getMatches(ref, size, option));
// Prevent too many rendering using useCallback
const handleSize = useCallback(() => {
setMatches(getMatches(ref, size, option));
}, [ref?.offsetHeight, ref?.offsetWidth]);
useIsomorphicLayoutEffect(() => {
handleSize();
// Listen matchMedia
if (window) {
window.addEventListener('resize', handleSize);
}
return () => {
if (window) {
window.removeEventListener('resize', handleSize);
}
};
}, [ref?.offsetWidth]);
return [setRef, matches];
}
export default useContainerMediaQuery;
partly extracted from useHooks
Related
I want to add dark mode in my React Native App. I have come across many implementations including React Navigation and Appearance. Everywhere I see code similar to the one below:-
import { Text, useColorScheme } from 'react-native';
const theme = useColorScheme();
<Text style={theme === 'light' ? styles.text_light : styles.text_dark}>Hello</Text>
But using these ternary operators everywhere is adding up to noise in code. I have created my custom Text and View as you can see below.
import { Text, useColorScheme } from 'react-native';
const theme = useColorScheme();
const CustomText = ({ styles, children }) => {
return (
<Text
style={[
theme === 'dark' ? { color: 'white' } : { color: 'black' },
styles,
]}>
{children}
</Text>
);
};
My Question is, Is my solution feasible? Can I replace React Native Components like Text and View(that get used everywhere) with custom components, or the ternary operator in React Components a better way. Thank you.
react-native-paper provides components that are colored using the theme stored in the PaperProvider context. React navigation has a similar context. Since these themes are compatible with one another, here's an example where users allow to manipulate both:
First merge the paper and navigation light and dark schemes:
// React native paper theme is compatible with react-navigation theme
// so merge them
const CombinedDefaultTheme = merge(
PaperDefaultTheme,
NavigationDefaultTheme
);
const CombinedDarkTheme = merge(PaperDarkTheme, NavigationDarkTheme);
Now I create a function that will customize the merged scheme base on a provided color and whether or not dark mode is desired:
// create theme base on provided color and whether in darkmode
export const getTheme = (color, isDarkMode) => {
// use color scheme generator to get colors
const [primary, accent, ...otherColors] =
colorSchemes.getNeutralScheme(color);
const colorPresets = isDarkMode
? darkenColorPresets
: lightenColorPresets;
// get lighter and darker versions of these colors
const primaryPresets = colorPresets(primary);
const accentPresets = colorPresets(accent);
// get default paper& navigation theme
const DefaultTheme = isDarkMode
? CombinedDarkTheme
: CombinedDefaultTheme;
//override theme with changes
const theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary,
accent,
primaryPresets,
accentPresets,
otherColors,
},
};
// make background color darker
const onDark = {
background: colorManipulators.blend(primary, "#000", 0.8),
};
// make text lighter version of background color
onDark.text = colorManipulators.blend(onDark.background, "#fff", 0.6);
if (isDarkMode) theme.colors = { ...theme.colors, ...onDark };
return theme;
};
And now put this theme into a piece of state with useReducer:
import React, { useReducer } from "react";
import { useColorScheme } from "react-native";
import { getTheme } from "../colors";
const initialState = {
mainColor: "#0D3FF8",
};
initialState.isDarkMode = false;
initialState.theme = getTheme(
initialState.mainColor,
initialState.isDarkMode
);
const reducer = (state, action) => {
switch (action.type) {
case "setMainColor": {
let newColor = action.payload;
return {
...state,
mainColor: newColor,
theme: getTheme(newColor, state.isDarkMode),
};
}
case "overrideTheme": {
let newTheme = action.payload;
return { ...state, theme: { ...state.theme, ...newTheme } };
}
case "setIsDark": {
const newDark = action.payload;
return {
...state,
isDarkMode: newDark,
theme: getTheme(state.mainColor, newDark),
};
}
}
};
export default function ThemeReducer(stateOverride = {}) {
console.log("here is the state override");
console.log(stateOverride);
const [state, dispatch] = useReducer(reducer, {
...initialState,
isDarkMode: useColorScheme() == "dark",
...stateOverride,
});
return { ...state, dispatch };
}
I am using mui5(Material Ui) in my nextjs application. I am trying to implementing dark mode. All are going well. I want a feature that if any user toggle the dark mode then it will be saved in local-storage. Then when I refresh the page, It automatically getting value from local-storage and active dark or light mode according to value from local-storage. If user first come to the site then it should active automatically system preference mode. I mean If there are no value in the local-storage then it should active automatically system preference mode. How can I do that.
Here is my code-
_app.js
export default function MyApp(props) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const [mode, setMode] = React.useState("light");
const colorMode = React.useMemo(
() => ({
// The dark mode switch would invoke this method
toggleColorMode: () => {
setMode((prevMode) =>
prevMode === 'light' ? 'dark' : 'light',
);
},
}),
[],
);
// Update the theme only if the mode changes
const muiTheme = React.useMemo(() => createTheme(theme(mode)), [mode]);
return (
<ColorModeContext.Provider value={colorMode}>
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider theme={muiTheme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
</ColorModeContext.Provider>
);
}
MyApp.propTypes = {
Component: PropTypes.elementType.isRequired,
emotionCache: PropTypes.object,
pageProps: PropTypes.object.isRequired,
};
Toogle button-
const theme = useTheme();
const colorMode = useContext(ColorModeContext);
<FormControlLabel
control={<MaterialUISwitch sx={{ m: 1 }} checked={theme.palette.mode === 'dark' ? false : true} />}
label=""
sx={{ mx: "0px" }}
onClick={colorMode.toggleColorMode}
/>
Here you can have an example of what I've done with Next.js and Material UI (5) to:
Have 2 themes available: lightTheme and darkTheme.
Have a ThemeSwitcherButton component so we can swtich between both themes.
Create a new ThemeProvider and ThemeContext to store the selected theme mode value, provide access to read and change it.
Store the preference of the user on Local Storage using a useLocalStorage hook.
Load the theme mode reading from the browser preference if there's no storage value, using the Material UI useMediaQuery hook.
I'm using Typescript, but it doesn't matter if you use plain JavaScript
Create the 2 themes needed:
We'll have 2 files to modify the specific properties independently.
darkTheme.ts
import { createTheme } from '#mui/material/styles'
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
})
export default darkTheme
lightTheme.ts
import { createTheme } from '#mui/material/styles'
const lightTheme = createTheme({
palette: {
mode: 'light',
},
})
export default lightTheme
Create the switcher button:
The important thing here is the value and function retrieved from the context, the button style or icon can be anything.
interface ThemeSwitcherButtonProps extends IconButtonProps { }
const ThemeSwitcherButton = ({ ...rest }: ThemeSwitcherButtonProps) => {
const { themeMode, toggleTheme } = useThemeContext()
return (
<Tooltip
title={themeMode === 'light' ? `Switch to dark mode` : `Switch to light mode`}
>
<IconButton
{...rest}
onClick={toggleTheme}
>
{themeMode === 'light' ? <DarkModeOutlined /> : <LightModeRounded />}
</IconButton>
</Tooltip>
)
}
export default ThemeSwitcherButton
Create the ThemeContext, ThemeProvider, and useThemeContext:
We use useMediaQuery from material ui library to check the preference mode of the browser. This hook works with client rendering and ssr.
We also use useLocalStorage hook to save the state in the local storage so it's persisted.
We wrap the original Material UI ThemeProvider (renamed as MuiThemeProvider) with this new Provider, so then in the _app file only one Provider is needed
ThemeContext.tsx
import { createContext, ReactNode, useContext } from 'react'
import { ThemeProvider as MuiThemeProvider, useMediaQuery } from '#mui/material'
import lightTheme from '#/themes/light'
import darkTheme from '#/themes/dark'
import useLocalStorage from '#/react/hooks/useLocalStorage'
const DARK_SCHEME_QUERY = '(prefers-color-scheme: dark)'
type ThemeMode = 'light' | 'dark'
interface ThemeContextType {
themeMode: ThemeMode
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)
const useThemeContext = () => useContext(ThemeContext)
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const isDarkOS = useMediaQuery(DARK_SCHEME_QUERY)
const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>('themeMode', isDarkOS ? 'light' : 'dark')
const toggleTheme = () => {
switch (themeMode) {
case 'light':
setThemeMode('dark')
break
case 'dark':
setThemeMode('light')
break
default:
}
}
return (
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
<MuiThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export {
useThemeContext,
ThemeProvider
}
_app.tsx
export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
const getLayout = Component.getLayout ?? ((page) => page)
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</CacheProvider>
)
}
Create useLocalStorage hook:
I took the source from here, and modified it a little bit so could work properly with Next.js due to ssr and client rendering mismatches.
The useLocalStorage also uses another useEventListener hook, to synchronize the changes on the value among all other tabs opened.
useLocalStorage.tsx
// edited from source: https://usehooks-ts.com/react-hook/use-local-storage
// to support ssr in Next.js
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import useEventListener from '#/react/hooks/useEventListener'
type SetValue<T> = Dispatch<SetStateAction<T>>
function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
// Read local storage the parse stored json or return initialValue
const readStorage = (): T => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? (parseJSON(item) as T) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error)
return initialValue
}
}
// Persists the new value to localStorage.
const setStorage: SetValue<T> = value => {
if (typeof window == 'undefined') {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`,
)
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(state) : value
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue))
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
}
// State to store the value
const [state, setState] = useState<T>(initialValue)
// Once the component is mounted, read from localStorage and update state.
useEffect(() => {
setState(readStorage())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setStorage(state)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
const handleStorageChange = () => {
setState(readStorage())
}
// this only works for other documents, not the current one
useEventListener('storage', handleStorageChange)
// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
useEventListener('local-storage', handleStorageChange)
return [state, setState]
}
export default useLocalStorage
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === 'undefined' ? undefined : JSON.parse(value ?? '')
} catch (error) {
console.log('parsing error on', { value })
return undefined
}
}
The useEventListener.tsx is exactly the same as is in the web.
The code
All of the code of this example can be found in my github repository that can be used as a starting point for a project with Typescript, Nextjs and Material UI.
You can also try a working example here.
toggleColorMode: () => {
//use this line for save in localStorage
localStorage.setItem("mode",mode=== 'light' ? 'dark' : 'light' )
setMode((prevMode) =>
prevMode === 'light' ? 'dark' : 'light',
);
},
then write a useEffect to set mode based on localStorage
useEffect(()=>{
if( localStorage.getItem("mode")){
setMode(localStorage.getItem("mode"))
}
},[])
I followed the answer of #giorgiline and everything was fine till the local storage part, which for me seems to be a bit too complicated.
What I did instead is in the ThemeContext.tsx looks like this:
This bit is different:
const [themeMode, setThemeMode] = useState("light");
useEffect(() => {
// reading the storage
const stored = localStorage.getItem("theme");
setThemeMode(stored ? JSON.parse(stored) : "light");
}, []);
function updateTheme(theme: string){
// setting the storage
setThemeMode(theme)
localStorage.setItem("theme", JSON.stringify(theme));
}
const toggleTheme = () => {
switch (themeMode) {
case 'light':
updateTheme("highContrast")
break
case 'highContrast':
updateTheme("light")
break
default:
}
}
Here the whole file
import {createContext, ReactNode, useContext, useEffect, useState} from 'react'
import {createTheme, ThemeProvider as MuiThemeProvider} from '#mui/material'
import lightTheme from "../styles/theme/lightTheme";
import hightContrastTheme from "../styles/theme/hightContrastTheme";
import theme from "#storybook/addon-interactions/dist/ts3.9/theme";
interface ThemeContextType {
themeMode: string
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)
const useThemeContext = () => useContext(ThemeContext)
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [themeMode, setThemeMode] = useState("light");
useEffect(() => {
const stored = localStorage.getItem("theme");
setThemeMode(stored ? JSON.parse(stored) : "light");
}, []);
function updateTheme(theme: string){
setThemeMode(theme)
localStorage.setItem("theme", JSON.stringify(theme));
}
const toggleTheme = () => {
switch (themeMode) {
case 'light':
updateTheme("highContrast")
break
case 'highContrast':
updateTheme("light")
break
default:
}
}
return (
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
<MuiThemeProvider theme={themeMode === 'light' ? createTheme(lightTheme) : createTheme(hightContrastTheme)}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export {
useThemeContext,
ThemeProvider
}
I am aware there are similar questions to this on Stack Overflow, but no existing answers have been able to help me. I have tried many combination of jest.mock()/jest.fn() with no improvement.
I am attempting to test a React component, Messenger, which imports and uses a custom hook, useWindowSize. The custom hook returns an object with two parameters, width and height. The Messenger component will return different information based on the width and height received via useWindowSize.
I want to mock useWindowSize, so that I can pass different widths and heights to Messenger and make assertions about the results shown to the user.
In my code below, I would expect that the mock would replace any return value from useWindowSize() with my hardcoded value (width of 800). However, this is not happening.
Messenger Component
import React from "react";
import useWindowSize from "../../hooks/useWindowSize/useWindowSize";
import Desktop from "./Desktop";
import Mobile from "./Mobile";
function Messenger() {
const screenSize = useWindowSize();
if (screenSize.width < 768) {
return (
<Mobile />
);
} else {
return (
<Desktop /> // inside Desktop component is a div with a test-id: "test-messenger-desktop"
);
}
}
export default Messenger;
useWindowSize custom React hook
import { useLayoutEffect, useState } from "react";
const useWindowSize = () => {
const [windowSize, setWindowSize] = useState({
width: 0,
height: 0,
});
useLayoutEffect(() => {
const updateSize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener("resize", updateSize);
updateSize();
return () => window.removeEventListener("resize", updateSize);
}, []);
return windowSize;
};
export default useWindowSize;
Test File
import { render, screen } from "#testing-library/react";
import Messenger from "./Messenger";
import useWindowSize from "../../hooks/useWindowSize/useWindowSize";
jest.mock("../../hooks/useWindowSize/useWindowSize", () => ({
useWindowSize: {
width: 800,
height: 800,
},
}));
describe("Messenger", () => {
beforeEach(() => {
render(<Messenger />);
});
it("renders Desktop component when screen width is greater than 800px", () => {
const desktop = screen.getByTestId("test-messenger-desktop");
expect(desktop).toBeTruthy();
});
});
Console output
Messenger › renders Desktop component when screen width is greater than 800px
TypeError: (0 , _useWindowSize.default) is not a function
11 | function Messenger({ setCurrentPage }) {
12 | const isFirstRender = useIsFirstRender();
> 13 | const screenSize = useWindowSize();
I can't follow the documentation of implementing Material UI's media queries because it's specified for a plain React app and I'm using NextJs. Specifically, I don't know where to put the following code that the documentation specifies:
import ReactDOMServer from 'react-dom/server';
import parser from 'ua-parser-js';
import mediaQuery from 'css-mediaquery';
import { ThemeProvider } from '#material-ui/core/styles';
function handleRender(req, res) {
const deviceType = parser(req.headers['user-agent']).device.type || 'desktop';
const ssrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: deviceType === 'mobile' ? '0px' : '1024px',
}),
});
const html = ReactDOMServer.renderToString(
<ThemeProvider
theme={{
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia },
},
}}
>
<App />
</ThemeProvider>,
);
// …
}
The reason that I want to implement this is because I use media queries to conditionally render certain components, like so:
const xs = useMediaQuery(theme.breakpoints.down('sm'))
...
return(
{xs ?
<p>Small device</p>
:
<p>Regular size device</p>
}
)
I know that I could use Material UI's Hidden but I like this approach where the media queries are variables with a state because I also use them to conditionally apply css.
I'm already using styled components and Material UI's styles with SRR. This is my _app.js
import NextApp from 'next/app'
import React from 'react'
import { ThemeProvider } from 'styled-components'
const theme = {
primary: '#4285F4'
}
export default class App extends NextApp {
componentDidMount() {
const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles && jssStyles.parentNode)
jssStyles.parentNode.removeChild(jssStyles)
}
render() {
const { Component, pageProps } = this.props
return (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
<style jsx global>
{`
body {
margin: 0;
}
.tui-toolbar-icons {
background: url(${require('~/public/tui-editor-icons.png')});
background-size: 218px 188px;
display: inline-block;
}
`}
</style>
</ThemeProvider>
)
}
}
And this is my _document.js
import React from 'react'
import { Html, Head, Main, NextScript } from 'next/document'
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()
}
}
render() {
return (
<Html lang="es">
<Head>
<link
href="https://fonts.googleapis.com/css?family=Comfortaa|Open+Sans&display=swap"
rel="stylesheet"
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
First a caveat -- I do not currently have any experience using SSR myself, but I have deep knowledge of Material-UI and I think that with the code you have included in your question and the Next.js documentation, I can help you work through this.
You are already showing in your _app.js how you are setting your theme into your styled-components ThemeProvider. You will also need to set a theme for the Material-UI ThemeProvider and you need to choose between two possible themes based on device type.
First define the two themes you care about. The two themes will use different implementations of ssrMatchMedia -- one for mobile and one for desktop.
import mediaQuery from 'css-mediaquery';
import { createMuiTheme } from "#material-ui/core/styles";
const mobileSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "0px"
})
});
const desktopSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "1024px"
})
});
const mobileMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: mobileSsrMatchMedia }
}
});
const desktopMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: desktopSsrMatchMedia }
}
});
In order to choose between the two themes, you need to leverage the user-agent from the request. Here's where my knowledge is very light, so there may be minor issues in my code here. I think you need to use getInitialProps (or getServerSideProps in Next.js 9.3 or newer). getInitialProps receives the context object from which you can get the HTTP request object (req). You can then use req in the same manner as in the Material-UI documentation example to determine the device type.
Below is an approximation of what I think _app.js should look like (not executed, so could have minor syntax issues, and has some guesses in getInitialProps since I have never used Next.js):
import NextApp from "next/app";
import React from "react";
import { ThemeProvider } from "styled-components";
import { createMuiTheme, MuiThemeProvider } from "#material-ui/core/styles";
import mediaQuery from "css-mediaquery";
import parser from "ua-parser-js";
const theme = {
primary: "#4285F4"
};
const mobileSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "0px"
})
});
const desktopSsrMatchMedia = query => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: "1024px"
})
});
const mobileMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: mobileSsrMatchMedia }
}
});
const desktopMuiTheme = createMuiTheme({
props: {
// Change the default options of useMediaQuery
MuiUseMediaQuery: { ssrMatchMedia: desktopSsrMatchMedia }
}
});
export default class App extends NextApp {
static async getInitialProps(ctx) {
// I'm guessing on this line based on your _document.js example
const initialProps = await NextApp.getInitialProps(ctx);
// OP's edit: The ctx that we really want is inside the function parameter "ctx"
const deviceType =
parser(ctx.ctx.req.headers["user-agent"]).device.type || "desktop";
// I'm guessing on the pageProps key here based on a couple examples
return { pageProps: { ...initialProps, deviceType } };
}
componentDidMount() {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles && jssStyles.parentNode)
jssStyles.parentNode.removeChild(jssStyles);
}
render() {
const { Component, pageProps } = this.props;
return (
<MuiThemeProvider
theme={
pageProps.deviceType === "mobile" ? mobileMuiTheme : desktopMuiTheme
}
>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
<style jsx global>
{`
body {
margin: 0;
}
.tui-toolbar-icons {
background: url(${require("~/public/tui-editor-icons.png")});
background-size: 218px 188px;
display: inline-block;
}
`}
</style>
</ThemeProvider>
</MuiThemeProvider>
);
}
}
MUI v5
You'd need two packages,
ua-parser-js - to parse the user agent device type. With this we can know whether a user is on mobile or desktop.
css-mediaquery - to provide an implementation of matchMedia to the useMediaQuery hook we have used everywhere.
// _app.js
import NextApp from 'next/app';
import parser from 'ua-parser-js'; // 1.
import mediaQuery from 'css-mediaquery'; // 2.
import { createTheme } from '#mui/material';
const App = ({ Component, pageProps, deviceType }) => {
const ssrMatchMedia = (query) => ({
matches: mediaQuery.match(query, {
// The estimated CSS width of the browser.
width: deviceType === 'mobile' ? '0px' : '1024px',
}),
});
const theme = createTheme({
// your MUI theme configuration goes here
components: {
MuiUseMediaQuery: {
defaultProps: {
ssrMatchMedia,
},
},
}
});
return (
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
);
};
App.getInitialProps = async (context) => {
let deviceType;
if (context.ctx.req) {
deviceType = parser(context.ctx.req.headers['user-agent']).device.type || 'desktop';
}
return {
...NextApp.getInitialProps(context),
deviceType,
};
};
export default App;
Points to consider:
The example is made up but illustrates the problem.
In actual application global storage is used and action changing is being emitted inside hover() method of itemTarget. Here, to imitate global
storage, window object is used.
Using ES7 decorators (or other
ES7 syntax) is not allowed.
So, the problem is that in implementation below, when dragging, endDrag() method of itemSource is not being called.
Possible solutions would be create different (but practically the same) components which differ just by item types, import those components to the Container component and mount depending on props.itemType – so, it's not an DRY option.
The questions are:
1. How to do it right? How to reuse and render a draggable component which have depended on Container's props itemType inside DragSource/DropTarget?
2. Why does the solution below not work? Why is the endDrag() method not being called?
Container.js:
import React, { Component } from 'react';
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Item from './Item';
import ItemDndDecorator from './ItemDndDecorator';
const style = {
width: 333,
};
class Container extends Component {
state = {some: true}
hoverHandle = () => {
this.setState({some: !this.state.some})
}
render() {
const Item1 = ItemDndDecorator(Item, 'item1')
const Item2 = ItemDndDecorator(Item, 'item2')
window.hoverHandle = this.hoverHandle
return (
<div style={style}>
<Item1>
<Item2>
some text 1
</Item2>
</Item1>
</div>
);
}
}
export default DragDropContext(HTML5Backend)(Container)
Item.js:
import React from 'react';
const style = {
border: '1px dashed gray',
padding: '1rem',
margin: '1rem',
cursor: 'move',
};
function Item(props) {
const { connectDragSource, connectDropTarget } = props;
return connectDragSource(connectDropTarget(
<div style={style}>
{props.children}
</div>,
));
}
export default Item
ItemDnDDecorator.js:
import { DragSource, DropTarget } from 'react-dnd';
const itemSource = {
beginDrag(props) {
console.log('begin drag');
return { id: props.id } ;
},
endDrag() {
console.log('end drag');
}
};
const itemTarget = {
hover() {
window.hoverHandle()
}
};
function ItemDndDecorator(component, itemType) {
return (
DropTarget(itemType, itemTarget, connect => ({
connectDropTarget: connect.dropTarget(),
}))(
DragSource(itemType, itemSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))(component))
)
}
export default ItemDndDecorator