Mocked dispatch being fired but state not changing - reactjs

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 }

Related

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?

How to navigate between screens in react without usage of libraries like react router

I want to know if is possible to navigate between screens, using like a context api, or something else, where I can get the "navigateTo" function in any component without passing by props. And of course, without the cycle dependency problem.
Example with the cycle dependency problem
NavigateContext.tsx:
import React, { createContext, useMemo, useReducer } from 'react'
import { Home } from './pages/Home'
interface NavigateProps {
navigateTo: (screenName: string) => void
}
export const navigateContext = createContext({} as NavigateProps)
const reducer = (state: () => JSX.Element, action: { type: string }) => {
switch (action.type) {
case 'home':
return Home
default:
throw new Error('Page not found')
}
}
export function NavigateContextProvider() {
const [Screen, dispatch] = useReducer(reducer, Home)
const value = useMemo(() => {
return {
navigateTo: (screenName: string) => {
dispatch({ type: screenName })
},
}
}, [])
return (
<navigateContext.Provider value={value}>
<Screen />
</navigateContext.Provider>
)
}
Home.tsx:
import React, { useContext, useEffect } from 'react'
import { Flex, Text } from '#chakra-ui/react'
import { navigateContext } from '../NavigateContext'
export function Home() {
const { navigateTo } = useContext(navigateContext)
useEffect(() => {
setTimeout(() => {
navigateTo('home')
}, 2000)
}, [])
return (
<Flex>
<Text>Home</Text>
</Flex>
)
}
Yes, this is possible, but you'll need to maintain the list of string view names independently from your mapping of them to their associated components in order to avoid circular dependencies (what you call "the cycle dependency problem" in your question):
Note, I created this in the TS Playground (which doesn't support modules AFAIK), so I annotated module names in comments. You can separate them into individual files to test/experiment.
TS Playground
import {
default as React,
createContext,
useContext,
useEffect,
useState,
type Dispatch,
type ReactElement,
type SetStateAction,
} from 'react';
////////// views.ts
// Every time you add/remove a view in your app, you'll need to update this array:
export const views = ['home', 'about'] as const;
export type View = typeof views[number];
export type ViewContext = {
setView: Dispatch<SetStateAction<View>>;
};
export const viewContext = createContext({} as ViewContext);
////////// Home.ts
// import { viewContext } from './views';
export function Home (): ReactElement {
const {setView} = useContext(viewContext);
useEffect(() => void setTimeout(() => setView('home'), 2000), []);
return (<div>Home</div>);
}
////////// About.ts
// import { viewContext } from './views';
export function About (): ReactElement {
const {setView} = useContext(viewContext);
return (
<div>
<div>About</div>
<button onClick={() => setView('home')}>Go Home</button>
</div>
);
}
////////// ContextProvider.tsx
// import {viewContext, type View} from './views';
// import {Home} from './Home';
// import {About} from './About';
// import {Etc} from './Etc';
// Every time you add/remove a view in your app, you'll need to update this object:
const viewMap: Record<View, () => ReactElement> = {
home: Home,
about: About,
// etc: Etc,
};
function ViewProvider () {
const [view, setView] = useState<View>('home');
const CurrentView = viewMap[view];
return (
<viewContext.Provider value={{setView}}>
<CurrentView />
</viewContext.Provider>
);
}

How to set InitialState in React Testing Library

I am writing a test where I need to render a component, but the rendering of my component is not working and I am receiving this error:
Uncaught [TypeError: Cannot read property 'role' of undefined].
This is because in the componentDidMount function in my component I am checking if this.props.authentication.user.role === 'EXPERT'. However, this.props.authentication has user as undefined.
This is the correct initialState for my program, but for the test I want to set my initialState to have a user object. That is why I redefine initialState in my test. However, the component does not render with that new initialState.
Here is the testing file:
import { Component } from '../Component.js';
import React from 'react';
import { MemoryRouter, Router } from 'react-router-dom';
import { render, cleanup, waitFor } from '../../test-utils.js';
import '#testing-library/jest-dom/extend-expect';
afterEach(cleanup)
describe('Component Testing', () => {
test('Loading text appears', async () => {
const { getByTestId } = render(
<MemoryRouter><Component /></MemoryRouter>,
{
initialState: {
authentication: {
user: { role: "MEMBER", memberID:'1234' }
}
}
},
);
let label = getByTestId('loading-text')
expect(label).toBeTruthy()
})
});
Here is the Component file:
class Component extends React.Component {
constructor(props) {
super(props)
this.state = {
tasks: [],
loading: true,
}
this.loadTasks = this.loadTasks.bind(this)
}
componentDidMount() {
if (
this.props.authentication.user.role == 'EXPERT' ||
this.props.authentication.user.role == 'ADMIN'
) {
this.loadTasks(this.props.location.state.member)
} else {
this.loadTasks(this.props.authentication.user.memberID)
}
}
mapState(state) {
const { tasks } = state.tasks
return {
tasks: state.tasks,
authentication: state.authentication
}
}
}
I am also using a custom render function that is below
import React from 'react'
import { render as rtlRender } from '#testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { initialState as reducerInitialState, reducer } from './_reducers'
import rootReducer from './_reducers'
import configureStore from './ConfigureStore.js';
import { createMemoryHistory } from 'history'
function render(ui, {
initialState = reducerInitialState,
store = configureStore({}),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// override render method
export { render }
Perhaps I am coming super late to the party but maybe this can serve to someone. What I have done for a Typescript setup is the following (all this is within test-utils.tsx)
const AllProviders = ({
children,
initialState,
}: {
children: React.ReactNode
initialState?: RootState
}) => {
return (
<ThemeProvider>
<Provider store={generateStoreWithInitialState(initialState || {})}>
<FlagsProvider value={flags}>
<Router>
<Route
render={({ location }) => {
return (
<HeaderContextProvider>
{React.cloneElement(children as React.ReactElement, {
location,
})}
</HeaderContextProvider>
)
}}
/>
</Router>
</FlagsProvider>
</Provider>
</ThemeProvider>
)
}
interface CustomRenderProps extends RenderOptions {
initialState?: RootState
}
const customRender = (
ui: React.ReactElement,
customRenderProps: CustomRenderProps = {}
) => {
const { initialState, ...renderProps } = customRenderProps
return render(ui, {
wrapper: (props) => (
<AllProviders initialState={initialState}>{props.children}</AllProviders>
),
...renderProps,
})
}
export * from '#testing-library/react'
export { customRender as render }
Worth to mention that you can/should remove the providers that doesn't make any sense for your case (like probably the FlagsProvider or the HeaderContextProvider) but I leave to illustrate I decided to keep UI providers within the route and the others outside (but this is me making not much sense anyway)
In terms of the store file I did this:
//...omitting extra stuff
const storeConfig = {
// All your store setup some TS infer types may be a extra challenge to solve
}
export const store = configureStore(storeConfig)
export const generateStoreWithInitialState = (initialState: Partial<RootState>) =>
configureStore({ ...storeConfig, preloadedState: initialState })
//...omitting extra stuff
Cheers! 🍻
I am not sure what you are doing in the configure store, but I suppose the initial state of your component should be passed in the store.
import React from 'react'
import { render as rtlRender } from '#testing-library/react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { initialState as reducerInitialState, reducer } from './_reducers'
import { createMemoryHistory } from 'history'
function render(
ui,
{
initialState = reducerInitialState,
store = createStore(reducer,initialState),
...renderOptions
} = {}
) {
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
}
// re-export everything
export * from '#testing-library/react'
// override render method
export { render }
I hope it will help you :)

ThemeUI's useThemeUI does not contain useColorMode

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);

call forwarding with history.push

I wrote here is the code
import React, { FC, Fragment, useEffect } from "react";
import { createBrowserHistory } from "history";
const history = createBrowserHistory();
const HandlerErr: FC<{ error: string }> = ({ error }) => {
useEffect(()=>{
const time = setTimeout(() => {history.push(`/`)}, 2000);
return(()=> clearTimeout(time));
},[error])
return (
<Fragment>
<div>{error}</div>
<div>{"Contact site administrator"}</div>
</Fragment>
);
};
I use the HandlerErr component to redirect. but for some reason it doesn't work history.push (/).I took a video
You need to use history form the react-router-dom
like
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
class Test extends Component {
render () {
const { history } = this.props
return (
<div>
<Button onClick={() => history.push('./path')}
</div>
)
}
}
export default withRouter(Test)
import React, { FC, useEffect } from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
interface OwnProps {
error: string;
}
type Props = OwnProps & RouteComponentProps<any>;
const HandlerErr: FC<Props> = ({ error, history }) => {
useEffect(() => {
const timeout = setTimeout(() => {
history.push(`/`);
}, 2000);
return () => {
clearTimeout(timeout);
};
}, [error]);
return (
<>
<div>{error}</div>
<div>Contact site administrator</div>
</>
);
};
export default withRouter(HandlerErr);

Resources