Hello I have a custom hook to define my theme
code:
export default function App() {
const [theme, setTheme] = usePersistedState('light');
const toggleTheme = () => {
setTheme(theme.type === 'light' ? 'dark' : 'light');
};
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<div className="App">
<button onClick={toggleTheme}>a</button>
</div>
</ThemeProvider>
);
and here is my hook:
import { darkTheme, lightTheme } from '../themes/index';
function usePersistedState(key) {
const [state, setState] = useState(() => {
switch (key) {
case 'dark':
return darkTheme;
case 'light':
return lightTheme;
default:
return lightTheme;
}
});
console.log(state);
useEffect(() => {
localStorage.setItem(key, JSON.stringify(state.type));
}, [key, state]);
return [state, setState];
}
export default usePersistedState;
basically every time my state changes I save it to local storage, but for some reason it only works the first time
when I try to change my theme the second time I do not enter my switch
edit:
as i show in the gif the first time i get an object because i enter my switch, but when i try to change the theme i get only a text cuz i dont enter my switch
The argument you provide to useState will apply only on the first render.
On the next renders, what you provide to setState function will be applied (you provide 'light' or 'dark', so state value will be 'light' or 'dark')
For your use case, in which you want the switch case to run every state change, useReducer is a better option than useState (in my opinion).
import { useReducer } from 'react';
import { darkTheme, lightTheme } from '../themes/index';
function usePersistedState(key) {
const [state, dispatch] = useReducer(reducer, null, getTheme(key)); // first argument is the reducer function. second argument is the initial state. third argument is an init function (for complex initialization)
const reducer = (state, action) => { // this will run on every state change (on every dispatch call)
return getTheme(action.type)();
}
const getTheme = (themeType) => () => {
switch (themeType) {
case 'dark':
return darkTheme;
case 'light':
return lightTheme;
default:
return lightTheme;
}
}
console.log(state);
useEffect(() => {
localStorage.setItem('themeType', JSON.stringify(state));
}, [state]);
return [state, dispatch];
}
export default usePersistedState;
And from your component you call dispatch like so:
export default function App() {
const [theme, setTheme] = usePersistedState('light');
const toggleTheme = () => {
setTheme(theme.type === 'light' ? {type: 'dark'} : {type: 'light'});
};
return (
<ThemeProvider theme={theme}>
<GlobalStyle />
<div className="App">
<button onClick={toggleTheme}>a</button>
</div>
</ThemeProvider>
);
I think you forgot to add the object wrapper on the toogle with { type: ... }
const toggleTheme = () => {
setTheme({type: theme.type === 'light' ? 'dark' : 'light'});
};
Related
I tried to pass description=Button props to Button Component using HOC.
so that, I expected to render like, `Button
But, Empty Button Elements is Rendered!
My codeSandBoxLink:enter link description here
Button.jsx
import React from 'react'
import withLoading from './withLoading'
function Button() {
return <button></button>
}
export default withLoading(Button)
withLoading.jsx
export default function withLoading(Component) {
const WithLoadingComponent = (props) => {
return <Component>{props.description}</Component>
);
};
return WithLoadingComponent;
App.jsx
return(
<div>
<Button description="button"><Button>
</div>
)
Thanks for any help.
At Button compnent, you need to use props and follow your code so that is props.description.
function Button(props) {
return <button>{props.description}</button>;
}
At withLoading HOC, you need to pass all props for Component.
//HOC Example
export default function withLoading(Component) {
const WithLoadingComponent = (props) => {
const [loading, setLoading] = React.useState(true);
console.log("props:", props.description);
//delay 3sec...
React.useEffect(() => {
const timer = setTimeout(() => {
setLoading(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
return loading ? <p>loading...</p> : <Component {...props} />;
};
return WithLoadingComponent;
}
I have been fork and then fixed it. You can refer by this link: https://codesandbox.io/s/strange-cerf-glxquu?file=/src/withLoading.jsx
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 have a component callled FileHeader. Everytime I resize a column in it, it dispatches an action to changes a state. This triggers a rerender.
ListeHeader.js
const ListHeader = props => {
const dispatch = useDispatch()
let headerRef = useRef(new Array())
const { header_ } = useSelector((state) => state.ui, shallowEqual)
const onResize = ( e, {event, node, size}) => {
dispatch(changeHeaderWidth(node.id, size.width))
}
return (
<HeaderBody>
{header_
.map((header) => {
const getRef = (element) => headerRef.current.push(element)
return (
<ResizableBox
axis="x"
width={header.width}
height={20}
key={header.id}
handle={<DragHandle id={header.id} />}
onResize={onResize}
minConstraints={[50, 20]}
maxConstraints={[300, 20]}
>
<Header
key={header.id}
width={header.width}
handleDrag={handleDrag}
onClick={handleSort(header.id)}
>
<HeaderText ref={getRef}>{header.name}</HeaderText>
</Header>
</ResizableBox>
)
})}
</HeaderBody>
)
}
This is my reducer
export default (state = initial_state, actions) => {
switch (actions.type) {
case consts.CHANGE_HEADER_WIDTH : return {
...state,
headerWidth: state.headerWidth.map((item) =>
item.id === actions.payload.id ? { ...item, width: actions.payload.neWidth}
: item),
}
break;
default: return state;
}
}
I'm not calling headerWidth state in my component it causes a rerender when it changes
From the docs
When passing a callback using dispatch to a child component, you may
sometimes want to memoize it with useCallback. If the child component
is trying to optimize render behavior using React.memo() or similar,
this avoids unnecessary rendering of child components due to the
changed callback reference.
import React, { useCallback } from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
const incrementCounter = useCallback(
() => dispatch({ type: 'increment-counter' }),
[dispatch]
)
return (
<div>
<span>{value}</span>
<MyIncrementButton onIncrement={incrementCounter} />
</div>
)
}
export const MyIncrementButton = React.memo(({ onIncrement }) => (
<button onClick={onIncrement}>Increment counter</button>
))
Consider the following example:
const useCounter = () => {
const [count, setCount] = useState(0);
return [ count, setCount ];
};
const Shower = () => {
const [ value ] = useCounter();
console.log(value); //stays 0
return value;
}
const Setter = () => {
const [ value, setValue ] = useCounter();
console.log(value); //updates on click
return <button onClick={() => setValue(value+1)}>
Add
</button>
}
const App = () => {
return (
<div className="App">
<Setter />
<Shower />
</div>
);
}
What am I doing wrong? I'd expect that it will use the same state no matter where and how many times it gets used, and if that state updates, it should update every component which uses it I think.
Any suggestions?
That's what react context api try to solve.
const CounterContext = React.createContext({
count: 0,
setCount: () => null
})
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{
count, setCount
}}>
{children}
</CounterContext.Provider>
)
}
const useCounter = () => {
return React.useContext(CounterContext)
};
useCounter will now provide you the same count and setCount in every component you call it.
To use it:
const Shower = () => {
const { count } = useCounter();
return count;
}
const Setter = () => {
const { count, setCount } = useCounter();
return <button onClick={() => setCount(count+1)}>
Add
</button>
}
const App = () => {
return (
<CounterProvider>
<div className="App">
<Setter />
<Shower />
</div>
</CounterProvider>
);
}
useState returns a pair of value and setter. A piece of data and a way to change it, but everytime you instantiate a new Component a new instace of this pair will be created as well. hooks are a great way to share statetul logic between components, not state itself. Shower get's called and a instance of useCounter is created. Setter gets called and a new instance is created. The structure is the same, the state is not.
To share state between components use props, redux or Context API
When sharing things between functional components, I like to use the pattern below, it is the redux-ish reusable version of Federkun's answer above:
// this component should be an ancestor of component sharing state
// note that it works no matter how deep your subcomponents are in the render tree
class SharedStateContextProvider extends React.Component {
/* static propTypes = {
sharedContext: PropTypes.object,
reducer: PropTypes.func,
initialState: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
} */
constructor(props) {
super(props);
this.state = {
contextValue: { state: props.initialState, dispatch: this.handleDispatch },
};
}
handleDispatch = (action) => {
const { reducer } = this.props;
const { contextValue: { state: sharedState } } = this.state;
const newState = reducer(sharedState, action);
if (newState !== sharedState) {
this.setState(
() => ({
contextValue: { state: newState, dispatch: this.handleDispatch }
})
);
}
}
render() {
const { sharedContext: Context, children } = this.props;
const { contextValue } = this.state;
return (
<Context.Provider value={contextValue}>
{children}
</Context.Provider>
);
}
}
// the actual shared context
const CounterContext = React.createContext();
// add as much logic as you want here to change the state
// as you would do with redux
function counterReducer(state, action) {
switch(action.type) {
case 'setValue':
return {
...state,
value: action.data
};
default:
return state;
}
}
// counterContext is a prop so the dependency in injected
const Shower = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value);
return state.value;
}
// counterContext is a prop so the dependency in injected
const Setter = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value); //updates on click
return <button onClick={() => dispatch({ type: 'setValue', data: state.value+1 })}>
Add
</button>
}
// the actual shared state
const initialCounterState = { value: 0 };
const App = () => {
return (
<div className="App">
<SharedStateContextProvider
sharedContext={CounterContext}
reducer={counterReducer}
initialState={initialCounterState}
>
<Setter counterContext={CounterContext} />
<Shower counterContext={CounterContext} />
</SharedStateContextProvider>
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm working on this project where the frontend is in React with UIkit for the user interface. The integration between the parts looks poorly implemented. I'm going to explain why. There is a Modal component, something like
export class Modal extends Component {
static getByName = name => UIkit.modal(`[data-modal-name='${name}']`)
static show = name => {
const modal = Modal.getByName(name)
if (modal) modal.show()
}
static hide = name => {
const modal = Modal.getByName(name)
if (modal) modal.hide()
}
render() {
// a modal
}
}
this is used in this way
export const LoginFormModal = props => (
<Modal name="login-form" className="login-form-modal" hideClose>
<LoginForm />
</Modal>
)
and show/hide is called programmatically where needed (even redux's actions)
Modal.hide("login-form")
this is in a Redux action, like this
export const login = credentials => {
return dispatch => {
dispatch(showLoader())
API.authentication.login(
credentials,
response => {
setCurrentUser(
Object.assign({}, response.user, { user_id: response.user.id })
)
Modal.hide("login-form")
dispatch(loginSucceded(response))
dispatch(hideLoader())
dispatch(push("/"))
dispatch(fetchNotificationsCounter())
},
error => {
dispatch(loginFailed(error))
dispatch(hideLoader())
}
)
}
}
This seems to work. Until you leave a component. When you come back to it, the second time the programmatically hide does not work anymore.
Anyone can lead me to how integrate the parts in a more react-appropriate way?
Using the parts of uikit which manipulate the dom (show, hide) is obviously hard to connect with React (and probably you shouldn't), however:
You need to move the call of the functions show and hide inside the Component by passing the bool of the state of the modal (eg. modalopen) . A good hook is the componentWillReceiveProps which can be used to check the previus props
componentWillReceiveProps(nextProps) {
if (nextProps.modalopen !== this.props.modalopen) {
if (nextProps.modalopen) {
getByName(...).show()
} else {
getByName(...).hide()
}
}
}
(this is inside the Modal class)
The thing I don't like and that is definitely not a "React-way" is that the code is mutating state directly from an action creator (!). From React docs:
For example, instead of exposing open() and close() methods on a
Dialog component, pass an isOpen prop to it.
So what if you had one modal that would be controlled by the redux state? Here is a possible implementation:
ModalWindow - will react to state changes and render depending what's in store:
import React from 'react';
import InfoContent from './InfoContent';
import YesOrNoContent from './YesOrNoContent';
import { MODAL_ACTION } from './modal/reducer';
class ModalWindow extends React.Component {
renderModalTitle = () => {
switch (this.props.modalAction) {
case MODAL_ACTION.INFO:
return 'Info';
case MODAL_ACTION.YES_OR_NO:
return 'Are you sure?';
default:
return '';
}
};
renderModalContent = () => {
switch (this.props.modalAction) {
case MODAL_ACTION.INFO:
return <InfoContent />;
case MODAL_ACTION.YES_OR_NO:
return <YesOrNoContent />;
default:
return null;
}
};
render() {
return (
this.props.isModalVisible ?
<div>
<p>{this.renderTitle()}</p>
<div>
{this.renderModalContent()}
</div>
</div>
:
null
);
}
}
export default connect((state) => ({
modalAction: state.modal.modalAction,
isModalVisible: state.modal.isModalVisible,
}))(ModalWindow);
modal reducer it will expose API to show/hide modal window in the application:
export const SHOW_MODAL = 'SHOW_MODAL';
export const HIDE_MODAL = 'HIDE_MODAL';
const INITIAL_STATE = {
isModalVisible: false,
modalAction: '',
};
export default function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case SHOW_MODAL:
return { ...state, isModalVisible: true, modalAction: action.modalAction };
case HIDE_MODAL:
return { ...state, isModalVisible: false };
default:
return state;
}
}
export const MODAL_ACTION = {
YES_OR_NO: 'YES_OR_NO',
INFO: 'INFO',
};
const showModal = (modalAction) => ({ type: SHOW_MODAL, modalAction });
export const hideModal = () => ({ type: HIDE_MODAL });
export const showInformation = () => showModal(MODAL_ACTION.INFO);
export const askForConfirmation = () => showModal(MODAL_ACTION.YES_OR_NO);
So basically you expose simple API in form of redux action-creators to control the state of your ModalWindow. Which you can later use like:
dispatch(showInformation())
...
dispatch(hideModal())
Of course, there could be more to it like optional configuration that would be passed to action creators or queue for modals.
I use a combination of a hook and a component for this.
Hook:
import { useState } from "react";
import UIkit from "uikit";
export default function useModal() {
const [isOpen, setIsOpen] = useState(false);
const [ref, setRef] = useState(null);
const open = (e) => {
UIkit.modal(ref).show();
setIsOpen(true);
};
const close = (e) => {
UIkit.modal(ref).hide();
UIkit.modal(ref).$destroy(true);
setIsOpen(false);
};
return [setRef, isOpen, open, close];
}
Component:
import React, { forwardRef } from "react";
const Modal = forwardRef(({ children, isOpen, full, close }, ref) => (
<div
ref={ref}
data-uk-modal="container: #root; stack: true; esc-close: false; bg-close: false"
className={`uk-flex-top ${full ? "uk-modal-container" : ""}`}
>
<div className="uk-modal-dialog uk-margin-auto-vertical">
<button
type="button"
className="uk-modal-close-default"
data-uk-icon="close"
onClick={close}
/>
{isOpen && children()}
</div>
</div>
));
export default Modal;
Consumption:
function Demo() {
const [ref, isOpen, open, close] = useModal();
return (
<div>
<button
type="button"
className="uk-button uk-button-default"
onClick={open}
>
upgrade
</button>
<Modal isOpen={isOpen} close={close} ref={ref} full>
{() => (
<div>
<div className="uk-modal-header">
<h2 className="uk-modal-title">title</h2>
</div>
<div className="uk-modal-body">
body
</div>
</div>
)}
</Modal>
</div>
);
}
Read more: https://reactjs.org/docs/integrating-with-other-libraries.html