ThemeUI's useThemeUI does not contain useColorMode - reactjs

I'm trying to use themes in Rebass, and it suggested Theme UI for theming. After following the guide on the following, I cannot get setColorMode to work in my storybook.
import useColorMode
import React from 'react'
import { ColorMode, ThemeProvider, useColorMode } from 'theme-ui'
const ThemeWrapper = (props) => {
const [colorMode, setColorMode] = useColorMode() // error
//...
}
I receive this as an error instead: [useColorMode] requires the ThemeProvider component
import useThemeUI
import { ColorMode, ThemeProvider, useThemeUI } from 'theme-ui'
const ThemeWrapper = (props) => {
const context = useThemeUI()
const { setColorMode } = context
//...
}
Later on, I have setColorMode is not a function
Examining this context using console.log, it contains the following:
{
components: Object { p: {…}, b: {…}, i: {…}, … }
emotionVersion: "10.0.27"
theme: null
}
useColorMode is nowhere to be found.
What am I doing wrong?
My current code:
.storybook/config.js
import React, { useEffect } from 'react'
import addons from '#storybook/addons';
import { addDecorator, configure } from '#storybook/react';
import { ColorMode, ThemeProvider, useThemeUI } from 'theme-ui'
import theme from '../theme'
const channel = addons.getChannel();
const ThemeWrapper = (props) => {
const context = useThemeUI()
const { setColorMode } = context
console.log(context)
const setDarkMode = isDark => setColorMode(isDark ? 'dark' : 'default')
useEffect(() => {
channel.on('DARK_MODE', setDarkMode);
return () => channel.removeListener('DARK_MODE', setDarkMode);
}, [channel, setColorMode]);
return (
<ThemeProvider theme={theme}>
<ColorMode/>
{props.children}
</ThemeProvider>
);
}
addDecorator(renderStory => <ThemeWrapper>{renderStory()}</ThemeWrapper>);
configure([
require.context('../components', true, /\.stories\.(jsx?|mdx)$/),
require.context('../stories', true, /\.stories\.(jsx?|mdx)$/)
], module);

I asked here: https://github.com/system-ui/theme-ui/issues/537 and I managed to correct my problematic code.
The error arises from the function useColorMode not being called inside a <ThemeProvider>.
I changed my config file to the following to mitigate the issue. And it fixed my problem.
import React, { useEffect } from 'react'
import addons from '#storybook/addons';
import { addDecorator, configure } from '#storybook/react';
import { ColorMode, ThemeProvider, useColorMode } from 'theme-ui'
import theme from '../theme'
const channel = addons.getChannel();
const ThemeChanger = () => {
const [colorMode, setColorMode] = useColorMode();
const setDarkMode = isDark => setColorMode(isDark ? 'dark' : 'default')
useEffect(() => {
channel.on('DARK_MODE', setDarkMode);
return () => channel.removeListener('DARK_MODE', setDarkMode);
}, [channel, setColorMode]);
return <div/>
}
const ThemeWrapper = ({ children }) => {
return (
<ThemeProvider theme={theme}>
<ThemeChanger/>
<ColorMode/>
{children}
</ThemeProvider>
);
}
addDecorator(renderStory => <ThemeWrapper>{renderStory()}</ThemeWrapper>);
configure([
require.context('../components', true, /\.stories\.(jsx?|mdx)$/),
require.context('../stories', true, /\.stories\.(jsx?|mdx)$/)
], module);

Related

Why does React re-render the components multiple times when using Zustand?

I think the Space.js and Loading.js components re-render far too many times. I can understand it up til the very last 4 re-renders. What is causing them is a mystery.
[slug].js
import { useRouter } from "next/router";
import AuthLayout from "#/components/layouts/AuthLayout";
import { useEffect, useState } from "react";
import axios from "#/src/lib/axios";
import Container from "#mui/material/Container";
import Typography from "#mui/material/Typography";
import useBearStore from "stores/pages";
import Loading from "#/components/layouts/Auth/Loading";
export default function Space() {
console.log('Space.js: function()');
const {setLoading} = useBearStore();
const router = useRouter();
const { slug } = router.query;
const [space, setSpace] = useState(null);
const getSpace = async () => {
const { data } = await axios.get(`api/spaces/${slug}`);
setSpace(data.data);
setLoading(false);
};
useEffect(() => {
console.log("Space.js: useEffect()");
getSpace();
}, [slug]);
return (
<Loading>
<Container maxWidth="sm">
<Typography variant="h1" component="h1" gutterBottom>
{space?.name}
</Typography>
</Container>
</Loading>
);
}
Space.getLayout = function getLayout(page) {
return <AuthLayout>{page}</AuthLayout>;
};
Loading.js
import useBearStore from "stores/pages";
import { LinearProgress } from "#mui/material";
import { useEffect } from 'react'
export default function Loading({children}) {
const {loading, setLoading} = useBearStore();
console.log('Loading.js: function()');
useEffect(() => {
console.log('Loading.js: useEffect()');
setLoading(true);
}, []);
return loading ? <LinearProgress /> : children;
}
pages.js
import create from 'zustand'
const useBearStore = create((set) => ({
loading: true,
setLoading: (value) => set((state) => ({ loading: value }))
}))
export default useBearStore;
next.config.js
module.exports = {
// reactStrictMode: true,
// images: {
// domains: ["images.pexels.com"],
// },
};

wrong Initial state in zustand in nextjs server side

I use zustand as a global state manager and I want use of the persisted states in server side of nextjs pages. but when I log the currently state values it will print default values (which the default values are null) and it does not log the updated states values.
The codes are here:
store.ts
/**
*? For more information about config zustand store in nextjs visit here:
*? https://github.com/vercel/next.js/blob/canary/examples/with-zustand/lib/store.js
*/
import { useLayoutEffect } from 'react';
import create, { StoreApi } from 'zustand';
import createContext from 'zustand/context';
import { devtools, persist, PersistOptions } from 'zustand/middleware';
import { createClinicSlice } from './slices/clinic';
import { createUserSlice } from './slices/user';
import type { IClinicSlice, IResetState, IUserSlice } from '$types';
const persistProperties: PersistOptions<any> = {
name: 'globalStorage',
getStorage: () => localStorage,
// ? should update version when app version updated
version: 0,
};
let store: any;
interface IInitialState {
clinics: null;
activeClinic: null;
user: null;
}
const getDefaultInitialState = (): IInitialState => ({
clinics: null,
activeClinic: null,
user: null,
});
const zustandContext = createContext<StoreApi<IClinicSlice & IUserSlice & IResetState>>();
export const Provider = zustandContext.Provider;
export const useStore = zustandContext.useStore;
export const initializeStore = (preloadedState: Record<string, any> = {}) => {
return create<IClinicSlice & IUserSlice & IResetState>()(
devtools(
persist(
(set) => ({
...getDefaultInitialState(),
...preloadedState,
// #ts-ignore
...createClinicSlice(set),
// #ts-ignore
...createUserSlice(set),
resetStore: () => set(getDefaultInitialState()),
}),
persistProperties
)
)
);
};
export function useCreateStore(
serverInitialState: Record<string, any>
): typeof initializeStore {
if (typeof window === 'undefined') {
return () => initializeStore(serverInitialState);
}
const isReusingStore = Boolean(store);
store = store ?? initializeStore(serverInitialState);
useLayoutEffect(() => {
// serverInitialState is undefined for CSR pages. It is up to you if you want to reset
// states on CSR page navigation or not. I have chosen not to, but if you choose to,
// then add `serverInitialState = getDefaultInitialState()` here.
if (serverInitialState && isReusingStore) {
store.setState(
{
// re-use functions from existing store
...store.getState(),
// but reset all other properties.
...(serverInitialState || getDefaultInitialState()),
},
true // replace states, rather than shallow merging
);
}
});
return () => store;
}
_app.tsx
import { useEffect } from 'react';
import CssBaseline from '#material-ui/core/CssBaseline';
import { ThemeProvider, jssPreset, StylesProvider } from '#material-ui/core/styles';
import i18n from 'i18next';
import { create } from 'jss';
import rtl from 'jss-rtl';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { I18nextProvider } from 'react-i18next';
import { ThemeProvider as SCThemeProvider } from 'styled-components';
import muiTheme from '$/assets/style/theme';
import { AuthProvider } from '$/components/contexts/auth';
import { ReactToastify } from '$/components/ReactToastify/ReactToastify';
import { Provider, useCreateStore } from '$store';
import '../../public/assets/style/index.css';
import 'leaflet/dist/leaflet.css';
import 'leaflet-geosearch/assets/css/leaflet.css';
import 'leaflet-geosearch/dist/geosearch.css';
import 'react-toastify/dist/ReactToastify.css';
import '$/utils/i18n.config';
const jss = create({
plugins: [...jssPreset().plugins, rtl()],
});
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side');
if (jssStyles) {
jssStyles.parentElement!.removeChild(jssStyles);
}
}, []);
const createStore = useCreateStore(pageProps.initialZustandState);
return (
<>
<Head>
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, user-scalable=no"
/>
</Head>
<Provider createStore={createStore}>
<I18nextProvider i18n={i18n}>
<ReactToastify />
</I18nextProvider>
<StylesProvider jss={jss}>
<ThemeProvider theme={muiTheme}>
<SCThemeProvider theme={muiTheme}>
<CssBaseline />
<AuthProvider>
<I18nextProvider i18n={i18n}>
<Component {...pageProps} />
</I18nextProvider>
</AuthProvider>
</SCThemeProvider>
</ThemeProvider>
</StylesProvider>
</Provider>
</>
);
}
export default MyApp;
dashboard/index.tsx
import { useEffect } from 'react';
import type { NextPage } from 'next';
import { useRouter } from 'next/router';
import { PageScrollableArea } from '$/components/index';
import ClinicDashboardLayout from '$/components/layout/clinicDashboardLayout/ClinicDashboardLayout';
import { isNil } from '$/utils/stringUtils';
import ClinicDashboard from '$/view/clinic/Dashboard';
import withPrivateRoute from '$/view/hocs/withPrivateRoute';
import { CLINICS_PAGE_URL } from '$constant';
import { initializeStore, useStore } from '$store';
const Dashboard: NextPage = (props) => {
return (
<ClinicDashboardLayout>
<PageScrollableArea>
<ClinicDashboard />
</PageScrollableArea>
</ClinicDashboardLayout>
);
};
export default withPrivateRoute(Dashboard);
export function getServerSideProps() {
const zustandStore = initializeStore();
console.log(`===> zustandStore.getState() ===>`, zustandStore.getState());
// if (isNil(zustandStore.getState().activeClinic)) {
// return {
// redirect: {
// destination: CLINICS_PAGE_URL,
// permanent: true,
// },
// };
// }
return {
props: {},
};
}
I find out the store variable in store.ts file always is undefined and its value will not update as values are located in local storage
Does anybody have a similar experience with this issue?

Mocked dispatch being fired but state not changing

I think i'm misunderstanding some concept about jest functions, I'm trying to test if after a click my isCartOpen is being set to true; the function is working, being called with the desired value.
The problem is that my state isn't changing at all. I tried to set a spy to dispatch but i really can't understand how spy works or if it's even necessary in this case
// cart-icons.test.tsx
import { render, screen, fireEvent } from 'utils/test'
import CartIcon from './cart-icon.component'
import store from 'app/store'
import { setIsCartOpen } from 'features/cart/cart.slice'
const mockDispatchFn = jest.fn()
jest.mock('hooks/redux', () => ({
...jest.requireActual('hooks/redux'),
useAppDispatch: () => mockDispatchFn,
}))
describe('[Component] CartIcon', () => {
beforeEach(() => render(<CartIcon />))
it('Dispatch open/close cart action when clicked', async () => {
const { isCartOpen } = store.getState().cart
const iconContainer = screen.getByText(/shopping-bag.svg/i)
.parentElement as HTMLElement
expect(isCartOpen).toBe(false)
fireEvent.click(iconContainer)
expect(mockDispatchFn).toHaveBeenCalledWith(setIsCartOpen(true))
// THIS SHOULD BE WORKING, BUT STATE ISN'T CHANGING!
expect(isCartOpen).toBe(true)
})
})
// cart-icon.component.tsx
import { useAppDispatch, useAppSelector } from 'hooks/redux'
import { selectIsCartOpen, selectCartCount } from 'features/cart/cart.selector'
import { setIsCartOpen } from 'features/cart/cart.slice'
import { ShoppingIcon, CartIconContainer, ItemCount } from './cart-icon.styles'
const CartIcon = () => {
const dispatch = useAppDispatch()
const isCartOpen = useAppSelector(selectIsCartOpen)
const cartCount = useAppSelector(selectCartCount)
const toggleIsCartOpen = () => dispatch(setIsCartOpen(!isCartOpen))
return (
<CartIconContainer onClick={toggleIsCartOpen}>
<ShoppingIcon />
<ItemCount>{cartCount}</ItemCount>
</CartIconContainer>
)
}
export default CartIcon
// utils/test.tsx
import React, { FC, ReactElement } from 'react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import { ApolloProvider } from '#apollo/client'
import { Elements } from '#stripe/react-stripe-js'
import { render, RenderOptions } from '#testing-library/react'
import store from 'app/store'
import { apolloClient, injectStore } from 'app/api'
import { stripePromise } from './stripe/stripe.utils'
injectStore(store)
const AllTheProviders: FC<{ children: React.ReactNode }> = ({ children }) => {
return (
<Provider store={store}>
<ApolloProvider client={apolloClient}>
<BrowserRouter>
<Elements stripe={stripePromise}>{children}</Elements>
</BrowserRouter>
</ApolloProvider>
</Provider>
)
}
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => render(ui, { wrapper: AllTheProviders, ...options })
export * from '#testing-library/react'
export { customRender as render }

What is causing this enqueue snackbar error for notistack?

I'm trying to use the useSnack hook from notistack library but I keep getting this error
TypeError: Cannot destructure property 'enqueueSnackbar' of 'Object(...)(...)' as it is undefined.
Here is the code:
import React, { useContext, useEffect } from "react";
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider, useSnackbar } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
// This line below is where the error seems to be
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
alertContext.msg !== "" &&
enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider maxSnack={4}>{props.children}</SnackbarProvider>;
};
export default Alerts;
useSnackbar hook accessible anywhere down the tree from SnackbarProvider.
So you cannot use it in the same component as SnackbarProvier.
import AlertContext from "../context/alert/alertContext";
import { SnackbarProvider } from "notistack";
const Alerts = (props) => {
const alertContext = useContext(AlertContext);
const providerRef = React.useRef();
useEffect(() => {
alertContext.msg !== "" &&
providerRef.current.enqueueSnackbar(alertContext.msg, {
variant: alertContext.type,
});
}, [alertContext]);
return <SnackbarProvider ref={providerRef} maxSnack={4}>
{props.children}
</SnackbarProvider>;
};
export default Alerts;
Wrap you index file with SnapBar provider:
index.js
import { SnackbarProvider } from "notistack";
const Index = () => (
<SnackbarProvider maxSnack={1} preventDuplicate>
index
</SnackbarProvider>
)
export default Index
jsx file
import { useSnackbar } from "notistack";
const Logs = () => {
const { enqueueSnackbar } = useSnackbar();
const handler = () => {
enqueueSnackbar(`Successful.`, { variant: "success" });
};
return <span onClick={handler}>"Logs loading"</span>;
};
export default Logs;

Next JS code inside getInitialProps not executes after page reload

I'm integrating NextJS into my React app. I face a problem, on page reload or opening direct link(ex. somehostname.com/clients) my getInitialProps not executes, but if I open this page using <Link> from next/link it works well. I don't really understand why it happens and how to fix it. I have already came throught similar questions, but didn't find any solution which could be suitable for me.
Clients page code:
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ClientsTable } from '../../src/components/ui/tables/client-table';
import AddIcon from '#material-ui/icons/Add';
import Fab from '#material-ui/core/Fab';
import { AddClientModal } from '../../src/components/ui/modals/add-client-modal';
import CircularProgress from '#material-ui/core/CircularProgress';
import { Alert } from '../../src/components/ui/alert';
import { Color } from '#material-ui/lab/Alert';
import { AppState } from '../../src/store/types';
import { thunkAddClient, thunkGetClients } from '../../src/store/thunks/clients';
import { SnackbarOrigin } from '#material-ui/core';
import { IClientsState } from '../../src/store/reducers/clients';
import { NextPage } from 'next';
import { ReduxNextPageContext } from '../index';
import { PageLayout } from '../../src/components/ui/page-layout';
const Clients: NextPage = () => {
const [addClientModalOpened, setAddClientModalOpened] = useState<boolean>(false);
const [alertType, setAlertType] = useState<Color>('error');
const [showAlert, setAlertShow] = useState<boolean>(false);
const alertOrigin: SnackbarOrigin = { vertical: 'top', horizontal: 'center' };
const dispatch = useDispatch();
const { clients, isLoading, hasError, message, success } = useSelector<AppState, IClientsState>(state => state.clients);
useEffect(() => {
if (success) {
handleAddModalClose();
}
}, [success]);
useEffect(() => {
checkAlert();
}, [hasError, success, isLoading]);
function handleAddModalClose(): void {
setAddClientModalOpened(false);
}
function handleAddClient(newClientName: string): void {
dispatch(thunkAddClient(newClientName));
}
function checkAlert() {
if (!isLoading && hasError) {
setAlertType('error');
setAlertShow(true);
} else if (!isLoading && success) {
setAlertType('success');
setAlertShow(true);
} else {
setAlertShow(false);
}
}
return (
<PageLayout>
<div className='clients'>
<h1>Clients</h1>
<div className='clients__add'>
<div className='clients__add-text'>
Add client
</div>
<Fab color='primary' aria-label='add' size='medium' onClick={() => setAddClientModalOpened(true)}>
<AddIcon/>
</Fab>
<AddClientModal
opened={addClientModalOpened}
handleClose={handleAddModalClose}
handleAddClient={handleAddClient}
error={message}
/>
</div>
<Alert
open={showAlert}
message={message}
type={alertType}
origin={alertOrigin}
autoHideDuration={success ? 2500 : null}
/>
{isLoading && <CircularProgress/>}
{!isLoading && <ClientsTable clients={clients}/>}
</div>
</PageLayout>
);
};
Clients.getInitialProps = async ({ store }: ReduxNextPageContext) => {
await store.dispatch(thunkGetClients());
return {};
};
export default Clients;
thunkGetClients()
export function thunkGetClients(): AppThunk {
return async function(dispatch) {
const reqPayload: IFetchParams = {
method: 'GET',
url: '/clients'
};
try {
dispatch(requestAction());
const { clients } = await fetchData(reqPayload);
console.log(clients);
dispatch(getClientsSuccessAction(clients));
} catch (error) {
dispatch(requestFailedAction(error.message));
}
};
}
_app.tsx code
import React from 'react';
import App, { AppContext, AppInitialProps } from 'next/app';
import withRedux from 'next-redux-wrapper';
import { Provider } from 'react-redux';
import { makeStore } from '../../src/store';
import { Store } from 'redux';
import '../../src/sass/app.scss';
import { ThunkDispatch } from 'redux-thunk';
export interface AppStore extends Store {
dispatch: ThunkDispatch<any, any, any>;
}
export interface MyAppProps extends AppInitialProps {
store: AppStore;
}
export default withRedux(makeStore)(
class MyApp extends App<MyAppProps> {
static async getInitialProps({
Component,
ctx
}: AppContext): Promise<AppInitialProps> {
const pageProps = Component.getInitialProps
? await Component.getInitialProps(ctx)
: {};
return { pageProps };
}
render() {
const { Component, pageProps, store } = this.props;
return (
<>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</>
);
}
}
);
Looking for your advices and help. Unfortunately, I couldn't find solution by myself.
This is the way Next.js works, it runs getInitialProps on first page load (reload or external link) in the server, and rest of pages that where navigated to with Link it will run this method on client.
The reason for this is to allow Next.js sites to have "native" SEO version.

Resources