I have made a high contrast mode but I have gotten stuck with trying to save the new value to local storage, any guidance I will be appreciated.
Here is the themes wrapper where I am setting up the theme
import React, { useState, useEffect } from 'react';
import { ThemeContext, themes } from '../context/themeContext';
export function isOdd(num) {
console.log("isOdd",num % 2);
}
export default function ThemeContextWrapper(props) {
const [theme, setTheme] = useState(themes.dark);
function changeTheme(theme) {
setTheme(theme);
}
useEffect(() => {
switch (theme) {
case themes.light:
document.body.classList.add('white-content');
// Store
// Retrieve
break;
case themes.dark:
default:
document.body.classList.remove('white-content');
localStorage.setItem(theme, themes.dark);
localStorage.getItem(theme);
break;
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme: theme, changeTheme: changeTheme }}>
{props.children}
</ThemeContext.Provider>
);
}
and here is the on click listener that triggers the high contrast mode
import React, { useState, useEffect } from "react";
import 'devextreme/dist/css/dx.common.css';
import 'devextreme/dist/css/dx.material.blue.light.css';
// Custom icons by Ionicons
import 'ionicons/dist/css/ionicons.css';
import DarkModeicon from '../images/icons/dark-mode-icon.png'
import SpeedDialAction from 'devextreme-react/speed-dial-action';
import { isOdd } from '../themewrapper/themeWrapperContext';
import Sun from '../images/icons/sun.png'
import config from 'devextreme/core/config';
import notify from 'devextreme/ui/notify';
import '../App.css';
class FloatingActionButton extends React.Component {
constructor(props) {
super(props);
config({
floatingActionButtonConfig: {
icon: 'runner',
closeIcon: 'icon ion-md-close',
position: {
my: 'right bottom',
at: 'right bottom',
offset: '-16 -16'
}
}
});
}
// function enabled night mode
toggleTheme() {
isOdd(3)
console.log("toggleTheme click func")
document.body.classList.add('dark-content');
localStorage.setItem(theme, themes.dark);
localStorage.getItem(theme, themes.dark);
}
toggleDay() {
isOdd(3)
console.log("toggle day click func")
document.body.classList.remove('dark-content');
document.body.classList.add('white-content');
}
render() {
return (
<div id="app-container">
<SpeedDialAction
hint="Turn on Day Mode"
icon={Sun}
onClick={() => this.toggleDay()}
/>
<SpeedDialAction
hint="Increase Font"
icon="growfont"
onClick={() =>
alert("Increase Font Clicked!!")
}
/>
<SpeedDialAction
hint="turn on night mode"
icon={DarkModeicon}
onClick={() => this.toggleTheme()}
/>
</div>
);
}
}
export default FloatingActionButton;
I tried to set up the local storage in both the on click listener and theme wrapper no success.
As using the Context.Provider' to provide the value to the child, you should also use Context.Consumer' in the child which needs to pick the value from provider.
So in FloatingActionButton component, you might use this instead:
class FloatingActionButton extends React.Component {
// ...some other code
toggleTheme(theme, changeTheme) {
let newTheme;
if (theme === themes.light) {
newTheme = themes.dark;
} else {
newTheme = themes.light;
}
changeTheme(newTheme); // set the theme immediately
localStorage.setItem('theme', newTheme); // then save it into localStorage
}
render() {
return (
<ThemeContext.Consumer>
{(theme, changeTheme) => (
// ...some other elements
<button onClick={() => toggleTheme(theme, changeTheme)}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
)
}
}
Then you can detect the theme in the ThemeContextWrapper component:
export default function ThemeContextWrapper(props) {
const [theme, setTheme] = useState(); // keep the default value as null because it will get the value in localStorage when useEffect detect the first render.
function changeTheme(theme) {
setTheme(theme);
}
useEffect(() => {
switch (theme) {
case themes.light:
document.body.classList.add('white-content');
break;
case themes.dark:
document.body.classList.remove('white-content');
default:
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme)
} else { // if there is no theme data in localStorage, set theme to dark mode
setTheme(themes.dark)
}
break;
}
}, [theme]);
return (
<ThemeContext.Provider value={{ theme: theme, changeTheme: changeTheme }}>
{props.children}
</ThemeContext.Provider>
);
}
Related
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 need your help.
I have an assignment, create two buttons(these buttons are initially white background).
When I click on one of them, I need both buttons to change color, using Redux.
The buttons I created. I don't know how to change the color.
Can anyone help? I would really appreciate it.
this is my code
import { COUNTER_GREEN, COUNTER_RED } from "../constants";
export const actionRed = () => ({
type: COUNTER_RED
});
export const actionGreen = () => ({
type: COUNTER_GREEN
});
=========================
import React from "react";
import { connect } from "react-redux";
import { actionGreen, actionRed } from "../../actions/counter";
const Counter = ({ actionRed, actionGreen }) => {
return (
<div>
<button onClick={actionRed}>Red</button>
<button onClick={actionGreen}>Green</button>
</div>
);
}
const mapStateToProps = () => ({});
const mapDispatchToProps = {
actionRed,
actionGreen
};
export const CounterContainer = connect(mapStateToProps,mapDispatchToProps)(Counter);
======================
const red = 'red';
const green = 'green';
const redtest = {
color: 'red'
}
export const counterReducer = (state={redtest}, action) => {
switch(action.type) {
case "COUNTER_RED": {
return {
state: {redtest}
};
}
case "COUNTER_GREEN": {
return {
state:green
};
}
default: {
return state;
}
}
};
create a buttonColor variable in your redux store. Import same variable as a prop in your component to give background to buttons. on button click dispatch an event which will change the buttonColor.
if you are not using this color property anywhere else in your application I would suggest you move it to the component state instead of redux store. If you are using functional component you can make use of hooks to update the state.
Please use hooks as below if you want to continue with functional component.
import React from "react";
import { connect } from "react-redux";
const Counter = () => {
const [btnColor, changeBtnColor] = useState('blue');
return (
<div>
<button onClick={()=>changeBtnColor('red')} style={{backgroundColor: btnColor}}>Red</button>
<button onClick={()=>changeBtnColor('green')} style={{backgroundColor: btnColor}}>Green</button>
</div>
);
}
export default Counter;
Try the below snippet if you want to switch to class-based component.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
btnColor: 'blue'
};
}
render() {
return (
<div>
<button onClick={()=>this.setState({this.state.btnColor: 'red'})} style={{backgroundColor: this.state.btnColor}}>Red</button>
<button onClick={()=>this.setState({this.state.btnColor: 'green'})} style={{backgroundColor: this.state.btnColor}}>Green</button>
</div>
);
}
}
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;
I have 3 components:
ComponentA
ComponentB
BackPressHandlingComponent
BackPressHandlingComponent deals with back press.
When back pressed from ComponentA; I must exit the app.
When back pressed from ComponentB; I must go to ComponentA.
Here is my BackPressHandlingComponent code -
import { BackHandler } from 'react-native';
export class BackPressHandlingComponent extends Component {
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}
}
My question is -
How do I tell BackPressHandlingComponent from Component A that I must exit app and from Component B that I need to go back to Component A
As per your use case, I would have addedBackpress event listeners on ComponentA and ComponentB, such that when you are on ComponentA when the callback is called you can exit the app and when in ComponentB its callback is called you can navigate to ComponentA.
Simple demo for above solution:
App.js
/**
*
* #format
* #flow
*/
import React, { Component } from 'react';
import { Text, View, TouchableOpacity, StyleSheet } from 'react-native';
import ComponentA from './ComponentA'
import ComponentB from './ComponentB'
import BackHandlerHOC from './BackHandlerHOC'
type Props = {};
export default class App extends Component<Props> {
state = {
render: 'A'
}
toggleComponent = () => {
let component = 'A'
if (this.state.render === 'A') {
component = 'B'
}
this.setState({ render: component })
}
render() {
const { render } = this.state
const wrappercomponent = render === 'A' ? (
<BackHandlerHOC
name="ComponentA"
Component={ComponentA}
/>
) : (
<BackHandlerHOC
name="ComponentB"
Component={ComponentB}
/>
)
return (
<View style={styles.container}>
<TouchableOpacity
onPress={() => this.toggleComponent()}
>
<Text> Change </Text>
</TouchableOpacity>
{wrappercomponent}
</View>
)
}
}
const styles = StyleSheet.create({
container: {
padding: 20
}
})
ComponentA
import React, { Component } from 'react';
import { View, Text } from 'react-native';
class ComponentA extends Component {
render() {
return (
<View>
<Text>A</Text>
</View>
);
}
}
export default ComponentA;
ComponentB
import React, { Component } from 'react';
import { View, Text } from 'react-native';
class ComponentB extends Component {
render() {
return (
<View>
<Text>B</Text>
</View>
);
}
}
export default ComponentB;
BackHandlerHOC
import React, { Component } from 'react';
import { BackHandler, ToastAndroid, View, Text } from 'react-native';
class BackHandlerHOC extends Component {
componentDidMount = () => {
BackHandler.addEventListener('hardwareBackPress', this.backPressHandler);
};
componentWillUnmount = () => {
BackHandler.removeEventListener('hardwareBackPress', this.backPressHandler);
};
backPressHandler = () => {
const { name } = this.props;
if (name === 'ComponentA') {
BackHandler.exitApp()
} else {
// this.props.navigator.resetTo({
// screen: 'ComponentA'
// })
ToastAndroid.show('will go back to A', 0);
}
return true;
};
render() {
const { Component } = this.props;
return (
<View>
<Text>Hello from backpress</Text>
<Component />
</View>
);
}
}
export default BackHandlerHOC;
You can also find the working example on expo here
Hope this helps
Just to add another approach,
I made use of the react-navigation lifecycle events,and the hardwareBackPress event, mind you the version of react-navigation here is 3.x.x.
The lifecycle event onWillFocus is called when the screen comes in view and the life-cycle event onWillBlur is called when the user is moving on to another screen, here somehow the React lifecycle events are in the hands of react-navigation, hence cannot use them here see https://reactnavigation.org/docs/3.x/navigation-lifecycle.
Following is the code:
import { BackHandler,Alert } from "react-native";
import { NavigationEvents } from 'react-navigation';
class SomeComponent {
//...my componentDidMount etc and other methods.....
backButtonAction(){
Alert.alert(
"Confirm Exit",
"Do you want to exit the app?",
[
{
text: "No",
onPress: () => {},
style: "cancel"
},
{ text: "Yes", onPress: () => BackHandler.exitApp() }
],
{ cancelable: false }
);
return true; // coz the event handler needs to return boolean.
};
setBackButtonAction(){
BackHandler.addEventListener(
"hardwareBackPress",
this.backButtonAction
);
}
removeBackButtonAction(){
BackHandler.removeEventListener(
"hardwareBackPress",
this.backButtonAction
);
}
render() {
return (
<Container>
<NavigationEvents
onWillFocus={payload => this.setBackButtonAction()}
onWillBlur={payload => this.removeBackButtonAction()}
/> //..... my view code
</Container>)
}
}
So I am having a problem after just typing 1 key into a textfield the textfield is re-rendering and losing focus.
Here are some snippets of code, if you need more do not hesitate to ask:
Here is the DiaryInput Component:
const DiaryInput = ({label, value, onChangeText, placeholder, secureTextEntry, autoCorrect, autoCapitalize}) => {
const {inputStyle, labelStyle, containerStyle} = styles;
return (
<View style={containerStyle}>
<Text style={labelStyle}>
{label}
</Text>
<TextInput
secureTextEntry={secureTextEntry}
placeholder={placeholder}
autoCorrect={autoCorrect}
autoCapitalize={autoCapitalize}
style={[inputStyle, {height: Platform.OS === 'android' ? 50 : 25}]}
value={value}
onChangeText={onChangeText}
keyboardType={'numeric'}
/*editable={false}*//>
</View>
)};
Here I am using the Component (within render()):
import React, {Component} from 'react';
import {ScrollView} from 'react-native';
import {DiaryInput} from "../DiaryInput";
import {DiarySlider} from "../DiarySlider";
import DiaryHeader from "../DiaryHeader";
import {TextArea} from "../common/TextArea";
import {connect} from 'react-redux';
import {
carbsChanged,
fatsChanged,
proteinChanged,
dailyWeightChanged,
hoursSleepChanged,
feelLevelChanged,
wodChanged,
notesChanged,
journalChanged,
dateChangedLeft,
dateChangedRight,
getDiaryInfo,
saveDiaryInfo
} from "../../actions/DiaryActions";
class DiaryPage extends Component {
componentWillMount() {
// this.props.getDiaryInfo();
// this.state.currentDateString = dateString;
this.props.currentDate = new Date();
}
onCarbsChanged(text) {
this.props.carbsChanged(text);
}
onFatsChanged(text) {
this.props.fatsChanged(text);
}
onProteinChanged(text) {
this.props.proteinChanged(text);
}
onDailyWeightChanged(text) {
this.props.dailyWeightChanged(text);
}
onHoursSleepChanged(text) {
this.props.hoursSleepChanged(text);
}
onFeelLevelChanged(value) {
this.props.feelLevelChanged(value);
}
onWODChanged(text) {
this.props.wodChanged(text);
}
onNotesChanged(text) {
this.props.notesChanged(text);
}
onJournalChanged(text) {
this.props.journalChanged(text);
}
onRightChanged(date) {
this.props.dateChangedRight(date);
}
onLeftChanged(date) {
this.props.dateChangedLeft(date);
}
render() {
return (
<ScrollView>
<DiaryHeader
text={this.props.currentDate.toLocaleString("en-US")}
onLeft={this.onLeftChanged(this.props.currentDate)}
onRight={this.onRightChanged(this.props.currentDate)}/>
<DiaryInput
label={"Carbs"}
value={this.props.carbs}
onChangeText={this.onCarbsChanged.bind(this)}/>
<DiaryInput
label={"Fat"}
value={this.props.fats}
onChangeText={this.onFatsChanged.bind(this)}/>
<DiaryInput
label={"Protein"}
value={this.props.proteins}
onChangeText={this.onProteinChanged.bind(this)}/>
<DiaryInput
label={"Daily Weight"}
value={this.props.dailyWeight}
onChangeText={this.onDailyWeightChanged.bind(this)}/>
<DiaryInput
label={"Hours Sleep"}
value={this.props.hoursSleep}
onChangeText={this.onHoursSleepChanged.bind(this)}/>
<DiarySlider
label={"Feel Level"}
value={this.props.feelLevel}
onChangeText={this.onFeelLevelChanged.bind(this)}/>
<TextArea
label={"WOD"}
value={this.props.wod}
onChangeText={this.onWODChanged.bind(this)}/>
<TextArea
label={"Notes"}
value={this.props.notes}
onChangeText={this.onNotesChanged.bind(this)}/>
<TextArea
label={"Journal"}
value={this.props.journal}
onChangeText={this.onJournalChanged.bind(this)}/>
</ScrollView>
)
}
}
function mapStateToProps({diary}) {
const {carbs, fats, proteins, dailyWeight, hoursSleep, feelLevel, wod, notes, journal, currentDate} = diary;
return {carbs, fats, proteins, dailyWeight, hoursSleep, feelLevel, wod, notes, journal, currentDate};
}
export default connect(mapStateToProps, {
carbsChanged,
fatsChanged,
proteinChanged,
dailyWeightChanged,
hoursSleepChanged,
feelLevelChanged,
wodChanged,
notesChanged,
journalChanged,
dateChangedLeft,
dateChangedRight,
getDiaryInfo,
saveDiaryInfo
})(DiaryPage);
Here I have the function for when text Changes:
onHoursSleepChanged(text) {
this.props.hoursSleepChanged(text);
}
The Action:
export const hoursSleepChanged = (text) => {
return {
type: HOURS_SLEEP_CHANGED,
payload: text
}};
Reducer:
case HOURS_SLEEP_CHANGED:
return {...state, hoursSleep: action.payload};
And I think that's all that is needed...
The action and reducer is working properly... Just the input is losing focus after a single keystroke...
Please let me know if you have an idea on this. I have been struggling to find the cause.
Thank you!