I am trying to use [Redux] to update the state in my app. I can't get the dispatch method to work in fetchUser. Spare me, I'm a bit of a noob.
All I want to do is have fetchUser run when [useEffect]runs on the first render.
The error I'm getting is
dispatch is not a function. (In 'dispatch({type:actions_types_WEBPACK_IMPORTED_MODULE_12_["FETCH_USER"], payload: result })', 'dispatch' is undefined)
Its the last useEffect in the main js file.
This is the index js file:
import 'react-app-polyfill/ie9';
import 'react-app-polyfill/stable';
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import App from './App';
import Main from './Main';
import './helpers/initFA';
import reducers from './reducers';
const store = createStore(reducers, {}, applyMiddleware(reduxThunk));
ReactDOM.render(
<Provider store={store}>
<Main>
<App />
</Main>
</Provider>
,
document.getElementById('main')
);
Here is the Main js file:
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import AppContext from './context/Context';
import { settings } from './config';
import toggleStylesheet from './helpers/toggleStylesheet';
import { getItemFromStore, setItemToStore, themeColors } from './helpers/utils';
import { connect } from 'react-redux';
import * as actions from './actions';
import axios from 'axios';
import { FETCH_USER } from './actions/types';
const Main = props => {
const [isFluid, setIsFluid] = useState(getItemFromStore('isFluid', settings.isFluid));
const [isRTL, setIsRTL] = useState(getItemFromStore('isRTL', settings.isRTL));
const [isDark, setIsDark] = useState(getItemFromStore('isDark', settings.isDark));
const [isTopNav, setIsTopNav] = useState(getItemFromStore('isTopNav', settings.isTopNav));
const [isCombo, setIsCombo] = useState(getItemFromStore('isCombo', settings.isCombo));
const [isVertical, setIsVertical] = useState(getItemFromStore('isVertical', settings.isVertical));
const [isNavbarVerticalCollapsed, setIsNavbarVerticalCollapsed] = useState(
getItemFromStore('isNavbarVerticalCollapsed', settings.isNavbarVerticalCollapsed)
);
const [currency, setCurrency] = useState(settings.currency);
const [showBurgerMenu, setShowBurgerMenu] = useState(settings.showBurgerMenu);
const [isLoaded, setIsLoaded] = useState(false);
const [isOpenSidePanel, setIsOpenSidePanel] = useState(false);
const [navbarCollapsed, setNavbarCollapsed] = useState(false);
const [navbarStyle, setNavbarStyle] = useState(getItemFromStore('navbarStyle', settings.navbarStyle));
const toggleModal = () => setIsOpenSidePanel(prevIsOpenSidePanel => !prevIsOpenSidePanel);
const value = {
isRTL,
isDark,
isCombo,
isFluid,
setIsRTL,
isTopNav,
currency,
setIsDark,
setIsCombo,
setIsFluid,
isVertical,
toggleModal,
setIsTopNav,
navbarStyle,
setCurrency,
setIsVertical,
showBurgerMenu,
setNavbarStyle,
isOpenSidePanel,
navbarCollapsed,
setShowBurgerMenu,
setIsOpenSidePanel,
setNavbarCollapsed,
isNavbarVerticalCollapsed,
setIsNavbarVerticalCollapsed
};
const setStylesheetMode = mode => {
setIsLoaded(false);
setItemToStore(mode, value[mode]);
toggleStylesheet({ isRTL, isDark }, () => setIsLoaded(true));
};
useEffect(() => {
setStylesheetMode('isFluid');
// eslint-disable-next-line
}, [isFluid]);
useEffect(() => {
setStylesheetMode('isRTL');
// eslint-disable-next-line
}, [isRTL]);
useEffect(() => {
setStylesheetMode('isDark');
// eslint-disable-next-line
}, [isDark]);
useEffect(() => {
setItemToStore('isNavbarVerticalCollapsed', isNavbarVerticalCollapsed);
// eslint-disable-next-line
}, [isNavbarVerticalCollapsed]);
useEffect(() => {
setItemToStore('isTopNav', isTopNav);
// eslint-disable-next-line
}, [isTopNav]);
useEffect(() => {
setItemToStore('isCombo', isCombo);
// eslint-disable-next-line
}, [isCombo]);
useEffect(() => {
setItemToStore('isVertical', isVertical);
// eslint-disable-next-line
}, [isVertical]);
useEffect(() => {
setItemToStore('navbarStyle', navbarStyle);
// eslint-disable-next-line
}, [navbarStyle]);
useEffect((dispatch) => {
const result = axios.get('/api/current_user')
dispatch({type: FETCH_USER, payload: result });
}, []);
if (!isLoaded) {
toggleStylesheet({ isRTL, isDark }, () => setIsLoaded(true));
return (
<div
style={{
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
backgroundColor: isDark ? themeColors.dark : themeColors.light
}}
/>
);
}
return <AppContext.Provider value={value}>{props.children}</AppContext.Provider>;
};
Main.propTypes = { children: PropTypes.node };
export default connect(null, actions)(Main);
Here is the App js file:
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import Layout from './layouts/Layout';
import 'react-toastify/dist/ReactToastify.min.css';
import 'react-datetime/css/react-datetime.css';
import 'react-image-lightbox/style.css';
const App = () => {
return (
<Router basename={process.env.PUBLIC_URL}>
<Layout />
</Router>
);
};
export default App;
Here is the client/src/layouts/Layout js file:
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { toast, ToastContainer } from 'react-toastify';
import { CloseButton, Fade } from '../components/common/Toast';
import DashboardLayout from './DashboardLayout';
import ErrorLayout from './ErrorLayout';
import loadable from '#loadable/component';
const AuthBasicLayout = loadable(() => import('./AuthBasicLayout'));
const Landing = loadable(() => import('../components/landing/Landing'));
const WizardLayout = loadable(() => import('../components/auth/wizard/WizardLayout'));
const AuthCardRoutes = loadable(() => import('../components/auth/card/AuthCardRoutes'));
const AuthSplitRoutes = loadable(() => import('../components/auth/split/AuthSplitRoutes'));
const Layout = () => {
useEffect(() => {
AuthBasicLayout.preload();
Landing.preload();
WizardLayout.preload();
AuthCardRoutes.preload();
AuthSplitRoutes.preload();
}, []);
return (
<Router fallback={<span />}>
<Switch>
<Route path="/" exact component={Landing} />
<Route path="/authentication/card" component={AuthCardRoutes} />
<Route path="/authentication/split" component={AuthSplitRoutes} />
<Route path="/authentication/wizard" component={WizardLayout} />
<Route path="/errors" component={ErrorLayout} />
<Route path="/authentication/basic" component={AuthBasicLayout} />
<Route path="/dashboard" component={DashboardLayout} />
</Switch>
<ToastContainer transition={Fade} closeButton={<CloseButton />} position=. {toast.POSITION.BOTTOM_LEFT} />
</Router>
);
};
export default Layout;
import { useDispatch } from 'react-redux';
const useFetching = (someFetchActionCreator) => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(someFetchActionCreator());
}, [])
}
For this example, I would recommend using useDispatch() from the react-redux function library. The above answer does not consider the async, and the result will simply return a Promise to the dispatch payload.
Related
I am trying to use context with my Gatsby project. I have successfully implemented this in my previous project and I have copied the code over to my new project and it's not working as intended.
This is my context.js file:
import React, { useContext, useState } from "react";
const defaultState = {
isLoggedIn: false,
};
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
function toggle() {
console.log("BOO!");
}
const value = {
isLoggedIn,
setIsLoggedIn,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
This is my app.js file:
import React from "react";
import { Router } from "#reach/router";
import IndexPage from "./index";
import ProjectPage from "./project";
import { AuthProvider } from "../contexts/context";
const App = () => (
<AuthProvider>
<Router basepath="/app">
<IndexPage path="/" component={IndexPage} />
<ProjectPage path="/project" component={ProjectPage} />
</Router>
</AuthProvider>
);
export default App;
This is my index.js file:
import React, { useContext } from "react";
import { Link } from "gatsby";
import { useAuth } from "../contexts/context";
import { AuthContext } from "../contexts/context";
const IndexPage = () => {
console.log(useAuth())
return (
<div className="w-40 h-40 bg-red-400">
{/*<Link to="/project">to projects</Link>*/}
<div>Click me to toggle: uh</div>
</div>
);
};
export default IndexPage;
useAuth() should return the desired components and functions but instead is always returning undefined. I have looked over my previous code as well as snippets on stack overflow and I can't seem to find the correct fix.
The following includes code that successfully built and executed:
Original context.js
import '#stripe/stripe-js'
/* Functionality */
import React, { useContext, useEffect, useState } from "react";
import { navigate } from "#reach/router";
import firebase from 'gatsby-plugin-firebase';
import { useLocalStorage } from 'react-use';
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [isLoading, setIsLoading] = useLocalStorage("loading", false);
// Sign In
const signInWithRedirect = (source) => {
let provider;
switch(source) {
case 'Google':
provider = new firebase.auth.GoogleAuthProvider()
break;
case 'Github':
provider = new firebase.auth.GithubAuthProvider()
break;
default:
break;
}
setIsLoading(true)
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION)
.then(() => {
// Existing and future Auth states are now persisted in the current
// session only. Closing the window would clear any existing state even
// If a user forgets to sign out.
// ...
// New sign-in will be persisted with session persistence.
return firebase.auth().signInWithRedirect(provider)
})
.catch((error) => {
// Handle Errors here.
let errorCode = error.code;
let errorMessage = error.message;
});
}
// Sign Out
const signOut = () => {
firebase.auth().signOut().then(() => {
// Sign-out successful.
setIsLoggedIn(false)
navigate('/app/login')
}).catch((error) => {
// An error happened.
});
}
useEffect(() => {
firebase.auth().onAuthStateChanged((user) => {
try {
// If user is authenticated
if (!!user) {
// Fetch firestore document reference
var docRef = firebase.firestore().collection("study_guide_customers").doc(user.uid)
docRef.get().then((doc) => {
console.log('checking doc')
// If the document doesn't exist, create it and add to the firestore database
if (!doc.exists) {
console.log('inside customer')
const customer = {
customerCreationTimestamp: firebase.firestore.Timestamp.now(),
username: user.displayName,
email: user.email
}
firebase.firestore().collection("study_guide_customers").doc(user.uid).set(customer)
.then(() => {
// After docuement for user is created, set login status
setIsLoggedIn(!!user)
setIsLoading(false)
})
.catch((error) => {
console.error("Error writing document: ", error);
});
// If document for user exists, set login status
} else {
setIsLoggedIn(!!user)
setIsLoading(false)
}
})
}
} catch {
console.log('Error checking firestore existence and logging in...')
}
})
}, [isLoggedIn, isLoading, setIsLoading, setIsLoggedIn])
const value = {
signOut,
isLoggedIn,
isLoading,
setIsLoading,
setIsLoggedIn,
signInWithRedirect,
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
Original app.js
/* Stripe Security */
import '#stripe/stripe-js'
/* Functionality */
import React from "react"
import { Router } from "#reach/router"
import PrivateRoute from "../components/PrivateRoute"
import Profile from "../components/Profile"
import Login from "../components/Login"
import Projects from "../components/Projects"
import IndexPage from "./index"
import NotFoundPage from './404'
import { AuthProvider } from "../contexts/context"
const App = () => (
<AuthProvider>
<Router basepath="/app">
<PrivateRoute path="/profile" component={Profile} />
<Login path="/login" component={Login}/>
<IndexPage path="/" component={IndexPage}/>
<Projects path="/projects" component={Projects} />
</Router>
</AuthProvider>
)
export default App
Original index.js
/* Stripe Security */
import '#stripe/stripe-js'
/* Functionality */
import * as React from "react"
import IndexContact from "../components/Index/Contact"
import IndexSelectedProjects from "../components/Index/SelectedProjects"
import IndexFeaturedProjects from "../components/Index/FeaturedProjects"
import IndexFooter from "../components/Index/Footer"
import IndexStudyGuide from "../components/Index/StudyGuide"
import IndexNavbar from "../components/Index/Navbar"
import IndexHeader from "../components/Index/Header"
import IndexAbout from '../components/Index/About'
import IndexExperience from '../components/Index/Experience'
import { useMount } from 'react-use';
const IndexPage = () => {
useMount(() => localStorage.setItem('loading', false));
return (
<>
<IndexNavbar />
<IndexHeader />
<IndexAbout />
<IndexExperience />
<IndexFeaturedProjects />
<IndexSelectedProjects />
<IndexStudyGuide />
<IndexContact />
<IndexFooter />
</>
)
}
export default IndexPage
Then in any component I could simply use the following code to access the context
import { useAuth } from "../contexts/context"
const { isLoggedIn, signInWithRedirect, isLoading } = useAuth()
Child components are mounted before parent. Fix your context.js file to add a default value for isLoggedIn state:
const defaultState = {
isLoggedIn: false,
setIsLoggedIn: () => {}
};
const AuthContext = React.createContext(defaultState);
Your defaultState should also include default methods for any parts of the context you wish to work with.
The code works but console log shows it renders twice. I want to reduce unnecessary rerenders/API fetching. Solutions tried include async-await, try-catch, useMemo, React.Memo, StrictMode in index.js (just quadruples the console log entries).
I prepared a codesandbox.
This is the console log:
Console logs twice every time
In the sandbox I use a fake API compared to my actual project code which uses Axios. The outcome is the same.
SearchContext (Context Provider) fetches from API once, then BlogList (Context Consumer) fetches again and console logs the second time.
React Version: 18.2.0
SearchContext.js
import React, { createContext, useState, useEffect, useMemo, useCallback } from 'react';
import axios from 'axios';
const SearchContext = createContext();
export const SearchContextProvider = ({ children }) => {
const [blog, setBlog] = useState([]);
const value = useMemo(() => ([ blog, setBlog ]), [blog]);
// const value = React.memo(() => ([ blog, setBlog ]), [blog]);
// I try 'useMemo, React.memo' (above):
// I try 'try-catch':
// const retrieveBlog = () => {
// try {
// const response = axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
// .then(response => {
// setBlog(response.data.items);
// console.log('thisiscontextprovider(blog):', blog)
// })
// }
// catch (err) {
// console.log(err);
// }
// }
// useEffect(() => {
// retrieveBlog();
// }, []);
// I try 'useCallback, if, async-await, try-catch' :
const retrieveBlog = useCallback(async () => {
if (blog.length === 0){
try {
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
.then(response => {
setBlog(response.data.items);
console.log('thisiscontextprovider(blog):', blog)
})
}
catch (err) {
console.log(err);
}
}
})
useEffect(() => {
retrieveBlog()
}, []);
// I try 'async-await':
// useEffect(() => {
// async function retrieveBlog() {
// try {
// const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
// .then(response => {
// setBlog(response.data.items);
// console.log('thisiscontextprovider(blog):', blog)
// })
// }
// catch (err) {
// console.log(err);
// }
// }
// retrieveBlog()
// }, []);
return (
<div>
<SearchContext.Provider value={value}>
{/* <SearchContext.Provider value={[blog, setBlog]}> */}
{children}
{console.log('from context provider return:', blog)}
</SearchContext.Provider>
</div>
);
};
export default SearchContext;
BlogList.js
import React, { useContext, useCallback, useEffect } from 'react';
import styled from 'styled-components';
import SearchContext from "../contexts/SearchContext";
import { SearchContextProvider } from "../contexts/SearchContext";
const BlogCard = styled.div`
`;
const BlogList = () => {
const [blog, setBlog] = useContext(SearchContext);
return (
<div>
<SearchContextProvider>
BlogList
<BlogCard>
{blog.map((blog) => (
<li key={blog.id}>
{blog.title}
</li>
))}
</BlogCard>
</SearchContextProvider>
</div>
);
};
export default BlogList;
App.js
import React from "react";
import { Route, Routes } from "react-router-dom";
import styled from "styled-components";
import BlogList from "./components/BlogList";
import { SearchContextProvider } from "./contexts/SearchContext";
const Wrapper = styled.h1``;
function App() {
return (
<Wrapper>
<p>
This CodeSandbox is for my question on StackOverflow. Console logs
twice.
</p>
<SearchContextProvider>
<Routes>
<Route exact path="" element={<BlogList />} />
{/* <Route exact path="/blog" element={<BlogList />} /> */}
</Routes>
</SearchContextProvider>
</Wrapper>
);
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from "react-router-dom";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
reportWebVitals();
I am keen to understand why this happens, rather than just solving it.
I am using context that updates my snackbar content from a single place. So whenever I want to display snackbar in some other component as a consumer, I update its value. Everything is functionally working but my tests are failing.
I am using mui snackbar.
For the reference: I am using react testing library coupled with jest.
Note: What I have done already:
waitFor with timeout property
Wrapped ContextProvider in render function
This is my code:
HOME.JS
import React from 'react';
import {Suspense} from 'react';
import {useState} from 'react';
import {Route} from 'react-router-dom';
import {Switch} from 'react-router-dom';
import {useLocation} from 'react-router-dom';
import withStyles from '#mui/styles/withStyles';
import BillingDetails from 'views/BillingDetails/BillingDetails';
import ErrorBoundary from 'containers/ErrorBoundary/ErrorBoundary';
import PageNotFound from 'views/PageNotFound/PageNotFound';
import SideBar from 'components/SideBar/SideBar';
import Snackbar from 'components/CustomSnackbar/CustomSnackbar';
import Unauthorized from 'views/Unauthorized/Unauthorized';
import {subRoutesConfig} from 'constants/routeConfig';
import {styles} from './styles';
import {BILLING_OVERDUE} from 'views/BillingDetails/constants';
import SidebarJobsContext from 'context/sidebarJobsContext';
import SnackbarContext from 'context/snackbarContext';
import {PropTypes} from 'prop-types';
const useStyles = (theme) => ({
...styles(theme),
});
const Home = (props) => {
let location = useLocation();
const [sidebarJobs, setSidebarJobs] = useState([]);
const [updateJobs, setUpdateJobs] = useState(false);
const [snackbar, setSnackbar] = useState({});
const sidebarJobsContextValue = {sidebarJobs, setSidebarJobs, updateJobs, setUpdateJobs};
const snackbarContextValue = {snackbar, setSnackbar};
const handleSnackbarClose = (reason, onClose) => {
if (reason == 'clickaway') {
return;
}
onClose && onClose();
setSnackbar({...snackbar, open: false});
};
const showSnackbar = () => {
const {
autoHideDuration,
text,
severity,
onExternalLink,
open=false,
onClose,
handleUndo,
} = snackbar;
return <Snackbar
autoHideDuration={autoHideDuration}
handleClose={(event, reason) => handleSnackbarClose(reason, onClose)}
handleExternalLink={onExternalLink}
handleUndo={handleUndo}
open={open}
severity={severity}
text={text}
/>;
};
const {
classes,
} = props;
const currentURLPath = location.pathname;
return (
<>
<SidebarJobsContext.Provider value={sidebarJobsContextValue}>
<SnackbarContext.Provider value={snackbarContextValue}>
<SideBar/>
<div className={classes.root}>
<div className={classes.offset}>
<ErrorBoundary key={currentURLPath}>
<Suspense fallback={<span></span>}>
<Switch>
{
(redirect || billingInfo.subscription_expiry) && <Route>
<BillingDetails/>
</Route>
}
{subRoutesConfig.map((route, i) => {
let component = isRouteAllowed(route) ? route.component : Unauthorized;
return (
<Route
component={component}
exact={route.exact}
key={route}
path={route.path}
/>
);
})}
<Route component={PageNotFound} />
</Switch>
{showProductTour()}
{showSnackbar()}
</Suspense>
</ErrorBoundary>
</div>
</div>
</SnackbarContext.Provider>
</SidebarJobsContext.Provider>
</>
);
};
Home.propTypes = {
classes: PropTypes.object,
};
export default withStyles(useStyles)(Home);
As you can see this is the Home.js component where I have added the provider and showSnackbar() takes care of displaying snackbar.
and then I am using useContext in functional and contextType in class components.
There are many tests that are failing here's an example of one:
import React from 'react';
import {fireEvent} from '#testing-library/react';
import {render} from '#testing-library/react';
import userEvent from '#testing-library/user-event';
import {waitFor} from '#testing-library/react';
import {convertIntoRGB} from 'utilities/utils';
import {ThemeWrapper} from 'utilities/testUtilities';
import {error} from 'constants/colors';
import {success} from 'constants/colors';
import CompanySettings from '../CompanySettings';
import SnackbarContext from '../../app/src/context/snackbarContext';
let snackbarContextValue = {
snackbar: {
text: '',
severity: 'success',
open: false,
autoHideDuration: 3000,
handleUndo: () => {},
onClose: () => {},
},
setSnackbar: () => {},
};
const component = (props) => {
return (render(<ThemeWrapper
storeIncluded={true}>
<SnackbarContext.Provider value={snackbarContextValue}>
<CompanySettings />
</SnackbarContext.Provider>
</ThemeWrapper>));
};
describe('CompanySettings', () => {
test('should Render success snackbar', async () => {
const {getByText, getByTestId, container, queryAllByTestId} = component();
expect(getByText('Company logo')).toBeInTheDocument();
expect(getByTestId('custom-icon-Package')).toBeVisible();
expect(queryAllByTestId('custom-skeleton').length).toBe(8);
await waitFor(()=> {
expect(queryAllByTestId('custom-skeleton').length).toBe(4);
let editBtn = getByText(/Edit company profile/i);
fireEvent.click(editBtn);
const inputField = container.querySelector(`input[name=companyName]`);
userEvent.type(inputField, ' 1');
fireEvent.click(getByTestId('saveBtn'));
});
await waitFor(() => {
expect(getByText('Gomez Inc 1')).toBeInTheDocument();
expect(container.querySelector(`input[name=companyName]`)).not.toBeInTheDocument();
});
await waitFor(() => {
expect(getByText(`Changes saved to 'company profile'.`)).toBeInTheDocument();
expect(getComputedStyle(getByTestId('alert')).backgroundColor).toEqual(convertIntoRGB(success));
});
});
});
The last waitFor in this test is failing as I have used context to display this snackbar content that says Changes saved to 'company profile'.
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 }
When I wrap the index.js with my context provider I just see a blank page.
this is my context
import React, { createContext, Fragment, useState } from 'react';
const FavoriteContext = createContext({
favorites: [],
addFav: (favoriteCar) => {},
removeFav: (carId) => {},
carIsFav: (carId) => {},
});
export const FavoriteCarsProvider = (props) => {
const [userFavorites, setUserFavorites] = useState([]);
const addFavoriteHandler = (favoriteCar) => {
setUserFavorites((prevFav) => {
return prevFav.concat(favoriteCar);
});
};
const removeFavoriteHandler = (carId) => {
setUserFavorites((prevFav) => {
return prevFav.filter((car) => car.id !== carId);
});
};
const carIsFavHandler = (carId) => {
userFavorites.some((car) => car.id === carId);
};
const favoritesValue = {
favorites: userFavorites,
addFav: addFavoriteHandler,
removeFav: removeFavoriteHandler,
carIsFav: carIsFavHandler,
};
return (
<FavoriteContext.Provider value={favoritesValue}>
<Fragment>{props.childen}</Fragment>
</FavoriteContext.Provider>
);
};
export default FavoriteContext;
when I wrap my provider in index.js I see a blank page.
and this is my index.js where I use my wrapper context provider.
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import { FavoriteCarsProvider } from './components/favorites/Favorite-context';
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<FavoriteCarsProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</FavoriteCarsProvider>
);
I got no error on the console. Just a blank page
You have a typo in the return of your Provider that is causing this.
return (
<FavoriteContext.Provider value={favoritesValue}>
<Fragment>{props.childen}</Fragment> // <- typo here, should be children.
</FavoriteContext.Provider>
);
Replace props.childen for props.children and you will be able to see your page.