I am new to testing react-redux. I have a component named OTARequestDetails where the state of reducers is used and I am trying to access that state but getting the following error:TypeError: Cannot read property 'otaRequestItem' of undefined. I have tried many different ways but I am not able to understand why I can't access otaRequestItem. I have tested action and reducers individually and both tests are passed now I want to test the component.
Component:
import React, { useEffect, useState } from 'react'
import OTARequestCommandDetails from '../OTARequestCommandDetails'
import { otaStatusEnum } from '../../../constants'
import OTACommonHeader from '../OTACommonHeader'
import {
SectionTitle,
OTAStatusIcon,
} from '../../../common/components/SDKWrapper'
import { useSelector } from 'react-redux'
import { useAuth0 } from '#auth0/auth0-react'
import OTAModal from '../../../common/components/OTAModal'
import constants from '../../../constants'
function OTARequestDetails(props) {
const { isAuthenticated } = useAuth0()
const [isModalOpen, setIsModalOpen] = useState(false)
const otaItem = useSelector((state) => state.otaReducer.otaRequestItem)
const _constants = constants.OTAModal
useEffect(() => {
if (!isAuthenticated) {
const { history } = props
history.push({
pathname: '/dashboard/ota',
})
}
}, [])
const onQuit = () => {
setIsModalOpen(false)
}
const onAbortClick = () => {
setIsModalOpen(true)
}
let abortInfo
if (otaItem && otaStatusEnum.IN_PROGRESS === otaItem.status) {
abortInfo = (
<div>
<span className="headerLabel"></span>
<span className="infoLabel">
OTA request is in-process of being delievered.
<br />
In-progress OTAs can be aborted. Please note this is irrevertible.
<span className="link" onClick={() => onAbortClick()}>
Abort in-progress OTA
</span>
</span>
</div>
)
}
return (
<div className="otaRequestDetails">
{otaItem && (
<div>
<OTACommonHeader title={'OTA Details'} />
<div className="container">
<div className="marginBottom20px">
<SectionTitle text={constants.Forms.iccId.title} />
</div>
<div>
<span className="headerLabel">Request ID:</span>
<span>{otaItem.id}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">ICCID to OTA:</span>
<span>{otaItem.iccId}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">Date Created:</span>
<span>{otaItem.createdDate}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">Date Delivered:</span>
<span>{otaItem.createdDate}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">Created By:</span>
<span>{otaItem.requestedBy}</span>
</div>
<div className="marginTop20px">
<span className="headerLabel">OTA Status:</span>
<span>
<OTAStatusIcon otaStatus={otaItem.status} />
{otaItem.status}
</span>
</div>
{abortInfo}
<hr className="seperateLine" />
<OTARequestCommandDetails otaItem={otaItem} />
</div>
</div>
)}
{isModalOpen && (
<OTAModal
_title={_constants.title}
_description={_constants.description}
_confirmText={_constants.confirmText}
_onQuit={onQuit}
isModalOpen={isModalOpen}
/>
)}
</div>
)
}
export default OTARequestDetails
Test:
import React from 'react'
import { Provider } from 'react-redux'
import { render, cleanup } from '#testing-library/react'
import thunk from 'redux-thunk'
import OTARequestDetails from '.'
import configureStore from 'redux-mock-store'
afterEach(cleanup)
const mockStore = configureStore([thunk])
describe('OTA Request Details', () => {
test('should render', () => {
const store = mockStore({
otaRequestItem: {
id: '20af3082-9a56-48fd-bfc4-cb3b53e49ef5',
commandType: 'REFRESH_IMSIS',
iccId: '789',
status: 'DELIEVERED',
requestedBy: 'testuser#telna.com',
createdDate: '2021-04-29T07:08:30.247Z',
},
})
const component = render(
<Provider store={store}>
<OTARequestDetails />
</Provider>,
)
expect(component).not.toBe(null)
})
})
Can anyone help me where I am wrong and why can't I access reducers? Thanks in advance.
With selector:
const otaItem = useSelector((state) => state.otaReducer.otaRequestItem);
You are accessing otaRequestItem from a state.otaReducer object.
In the test your mock store has no otaReducer property in its object. Nest otaRequestItem object within a otaReducer object.
const store = mockStore({
otaReducer: {
otaRequestItem: {
id: '20af3082-9a56-48fd-bfc4-cb3b53e49ef5',
commandType: 'REFRESH_IMSIS',
iccId: '789',
status: 'DELIEVERED',
requestedBy: 'testuser#telna.com',
createdDate: '2021-04-29T07:08:30.247Z',
},
},
});
Basic gist is... the mock store just needs a valid object shape for what a consuming component will attempt to select from it.
Related
On my component render, my useEffects hooks called, a function. the function updates the state status depending on the condition within the useEffects produce.
So in this case how to test the `mobileMenu` and how to set different condition in useEffect to test it?
I hope both my useEffects and useState need to mocked. I am in learning process with react. I could not get any correct answer upon searching, any one help me please?
here is my app.tsx
my ts file:
import { Footer, Header, ProductCart, ProductPhotoGallery, Tabs } from '#mcdayen/components';
import { Cart, Logo, MobileMenu, NaviLinks, QuickSearch, User } from '#mcdayen/micro-components';
import { initialNaviLinksProps, initialPhotoProps, initialTabsProps, NaviLinksProps, sizeProps } from '#mcdayen/prop-types';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchCartDetails, sizeHandler } from './store/cart.slice';
import { AppDispatch, RootState } from './store/store.config';
export function App() {
const dispatch:AppDispatch = useDispatch();
dispatch(fetchCartDetails());
const {product} = useSelector((state:RootState) => state.cartStore)
const [mobileMenu, setMobileMenu] = useState<boolean>(false);
const [linkProps, setLinkProps] = useState<NaviLinksProps | null>(null);
function mobileMenuHandler() {
setMobileMenu((current: boolean) => !current);
}
useEffect(() => {
setLinkProps(initialNaviLinksProps);
const mobileId = document.getElementById('mobileMenu');
if (mobileId?.offsetParent) {
mobileMenuHandler();
}
}, []);
useEffect(() => {
setLinkProps((props) => {
return mobileMenu ? { ...initialNaviLinksProps } : { ...initialNaviLinksProps, classProps: props?.classProps + ' hidden' }
})
}, [mobileMenu]);
function onSizeSelect(selectedSize: sizeProps) {
dispatch(sizeHandler(selectedSize));
}
return (
<section className="box-border m-auto flex flex-col pl-[18px] py-6 min-h-screen flex-wrap px-5 md:container md:w-[1440px] md:pl-[70px] pr-5 ">
<Header>
<Logo />
{linkProps && <NaviLinks passNaviLinks={linkProps} />}
<div className="flex gap-3">
<QuickSearch />
<Cart />
<User />
<MobileMenu menuHandler={mobileMenuHandler} />
</div>
</Header>
<main className='flex flex-col justify-between lg:flex-row'>
<div className='hidden lg:block w-[325px]'>
<div>
<Tabs tabProps={initialTabsProps} />
</div>
</div>
<div className='grow-0 flex-auto' >
{initialPhotoProps.length && <ProductPhotoGallery gallery={initialPhotoProps} />}
</div>
<div className='flex bg-white'>
{product && <ProductCart sizeSelect={onSizeSelect} passCartProps={product} />}
</div>
</main>
<Footer />
</section>
);
}
export default App;
My spec:
import { configureStore } from '#reduxjs/toolkit';
import { render } from '#testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import App from './app';
import cartReducer from './store/cart.slice';
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));
export function createTestStore() {
const store = configureStore({
reducer: {
cartStore:cartReducer,
}
})
return store;
}
describe('App', () => {
const setMobileMenu = jest.fn();
const useStateMock = (initState: boolean) => [initState, setMobileMenu];
jest.spyOn(React, 'useState').mockImplementation(useStateMock);
afterEach(() => {
jest.clearAllMocks();
});
const store = createTestStore();
it('should render successfully', () => {
const { baseElement } = render(
<BrowserRouter>
<Provider store={store}>{<App />}</Provider>
</BrowserRouter>
);
expect(baseElement).toBeTruthy();
useStateMock(true);
expect(setMobileMenu).toHaveBeenCalledWith(true);
});
});
I am getting an error at: `
jest.spyOn(React, 'useState').mockImplementation(useStateMock);
`
as : Argument of type '(initState: boolean) => (boolean | jest.Mock<any, any>)[]' is not assignable to parameter of type '() => [unknown, Dispatch<unknown>]'.
and my test failing.
Need help for:
test the useEffect hook on anonymous function ( mocking )
fixing the error highlighted
testing the state on setMobileMenu
Any one please help me with the correct way?
Try to declare useStateMock as:
const useStateMock = (initState: any) => [initState, setMobileMenu];
By using console.log(responseData.places) I have checked the fetching works since I am using a hook for this and seems to work fine until I setLoadedPlaces with is the method I use to update the loadedPlaces which I later use to get the values to fill the frontend part of the website.
This is the output I get from this console.log I did and the values are correct.
[{…}]
0: address: "sis se puede
busrespect: 'tu puedes',
creator: "6384e2f543f63be1c560effa"
description: "al mundial"
id: "6384e30243f63be1c560f000"
image:"https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Empire_State_Building_%28aerial_view%29.jpg/400px-Empire_State_Building_%28aerial_view%29.jpg"location: {lat: -12.086158, lng: -76.898019}
title: "Peru"
__v: 0
_id: "6384e30243f63be1c560f000"[[Prototype]]:
Objectlength: 1[[Prototype]]: Array(0)
So after this this the code I have in the frontend (SINCE the backend works properly) Let me know if you have any doubts with this logic
This is UserPlaces.js
import React, {useState, useEffect } from 'react';
import PlaceList from '../components/PlaceList';
import { useParams } from 'react-router-dom';
import { useHttpClient } from '../../shared/hooks/http-hook';
import ErrorModal from '../../shared/components/UIElements/ErrorModal';
import LoadingSpinner from '../../shared/components/UIElements/LoadingSpinner';
const UserPlaces = () => {
const {loadedPlaces, setLoadedPlaces} = useState();
const {isLoading, error, sendRequest, clearError } = useHttpClient();
const userId = useParams().userId;
useEffect(() => {
const fetchPlaces = async () => {
try {
const responseData = await sendRequest(
`http://localhost:5000/api/places/user/${userId}`
);
console.log(responseData.bus_stops)
setLoadedPlaces(responseData.bus_stops);
} catch (err) {}
};
fetchPlaces();
}, [sendRequest, userId]);
return (
<React.Fragment>
<ErrorModal error={error} onClear={clearError} />
{isLoading && (
<div className="center">
<LoadingSpinner />
</div>
)}
{!isLoading && loadedPlaces && <PlaceList items={loadedPlaces} />}
</React.Fragment>
);
};
export default UserPlaces;
This is Place-List.js
import React from 'react';
import "./PlaceList.css"
import Card from '../../shared/components/UIElements/Card'
import PlaceItem from './PlaceItem';
import Button from '../../shared/components/FormElements/Button';
const PlaceList = props => {
if (props.items.length === 0) {
return (
<div className='place-list-center'>
<Card>
<h2>No bus stops available. Be the first one to create one!</h2>
<Button to='/places/new'> Create Bus Stop </Button>
</Card>
</div>
);
}
return (
<ul className="place-list">
{props.items.map(bus_stops => (
<PlaceItem
key={bus_stops.id}
id={bus_stops.id}
image={bus_stops.image}
title={bus_stops.title}
busrespect={bus_stops.busrespect}
description={bus_stops.description}
address={bus_stops.address}
creatorId={bus_stops.creator}
coordinates={bus_stops.location}
/>
))}
</ul>
);
};
export default PlaceList;
This is PlaceItem.js
import React, { useState } from 'react';
import { useContext } from 'react';
import Card from '../../shared/components/UIElements/Card';
import Button from '../../shared/components/FormElements/Button';
import Modal from '../../shared/components/UIElements/Modal';
import Map from '../../shared/components/UIElements/Map';
import {AuthContext} from '../../shared//context/auth-context'
import "./PlaceItem.css";
const PlaceItem = props => {
const auth = useContext(AuthContext);
const [showMap, setShowMap] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const openMapHandler = () => setShowMap(true);
const closeMapHandler = () => setShowMap(false);
const showDeleteWarningHandler = () => {
setShowConfirmModal(true);
};
const cancelDeleteHandler = () => {
setShowConfirmModal(false);
};
const confirmDeleteHandler = () => {
setShowConfirmModal(false); //when clicked close the new Modal
console.log('DELETING...');
};
return (
<React.Fragment>
<Modal show={showMap}
onCancel={closeMapHandler}
header={props.address}
contentClass="place-item__modal-content"
footerClass="place-item__modal-actions"
footer={<Button onClick={closeMapHandler}>Close </Button>}
>
<div className='map-container'>
<Map center={props.coordinates} zoom={16}/> {/* Should be props.coordinates but we writing default data for now until geocoding solved. */}
</div>
</Modal>
<Modal
show={showConfirmModal}
onCancel={cancelDeleteHandler}
header="Are you entirely sure?"
footerClass="place-item__modal-actions"
footer={
<React.Fragment>
<Button inverse onClick={cancelDeleteHandler}>
CANCEL
</Button>
<Button danger onClick={confirmDeleteHandler}>
DELETE
</Button>
</React.Fragment>
}
>
<p>
Do you want to proceed and delete this place? Please note that it
can't be undone thereafter.
</p>
</Modal>
<li className='"place=item'>
<Card className="place-item__content">
<div className='place-item__image'>
<img src={props.image} alt={props.title}/>
</div>
<div className='place-item__info'>
<h2>{props.title}</h2>
<h3>{props.address}</h3>
<p>{props.description}</p>
<p>{props.busrespect}</p>
</div>
<div className='place-item__actions'>
<Button inverse onClick={openMapHandler}> VIEW ON MAP</Button>
{auth.isLoggedIn && (<Button to={`/places/${props.id}`}> EDIT</Button> )}
{auth.isLoggedIn &&<Button danger onClick={showDeleteWarningHandler}> DELETE </Button>}
</div>
</Card>
</li>
</React.Fragment>
);
};
export default PlaceItem;
This is auth-context:
import { createContext } from "react";
export const AuthContext = createContext({
isLoggedIn: false,
userId: null,
login: () => {},
logout: () => {}});
This is is Modal.js
import React from 'react';
import ReactDOM from 'react-dom';
import Backdrop from './Backdrop';
import { CSSTransition } from 'react-transition-group';
import './Modal.css';
const ModalOverlay = props => {
const content =(
<div className={`modal ${props.className}`} style = {props.style}>
<header className={`modal__header ${props.headerClass}`}>
<h2>{props.header}</h2>
</header>
<form
onSubmit={
props.onSubmit ? props.onSubmit : event => event.preventDefault()
}
>
<div className={`modal__content ${props.contentClass}`}>
{props.children}
</div>
<footer className={`modal__content ${props.footerClass}`}>
{props.footer}
</footer>
</form>
</div>
);
return ReactDOM.createPortal(content, document.getElementById('modal-hook'));
};
const Modal = props => {
return (
<React.Fragment>
{props.show && <Backdrop onClick={props.onCancel} />}
<CSSTransition in={props.show}
mountOnEnter
unmountOnExit
timeout={200}
classNames="modal"
>
<ModalOverlay {...props}/>
</CSSTransition>
</React.Fragment>
);
};
export default Modal;
Also Trust the routing is correct since I have checked it already and I am just wondering if the logic in REACT with loadedPlaces, PlaceItema and PlaceList makes sense and it working. Let me know please. It will be really helpful.
Summary: Not getting any error but no visual data appears in the scren just the header of my website and the background (rest is empty) even though logic is functional.
const {loadedPlaces, setLoadedPlaces} = useState();
change the above line to
const [loadedPlaces, setLoadedPlaces] = useState();
In my current project I'm using React Context to save component references so that the header component can access them to scroll to them. I managed to successfully make it work the first time with contactRef. But when I tried to add more states to the context, they just would not register.
Console logging the context in Header.js gives me;
contactRef: {current: div.contact}
dispatch: ƒ ()
findings: undefined
locationRef: undefined
[[Prototype]]: Object
I've attached the segments involved with this, but I've narrowed down the issue to be with the INITIAL_STATE in ComponentContext.js. Adding more states does not seem to work, every time only contactRef seems to be initialised.
ComponentContext.js
import { createContext, useReducer } from "react";
const INITIAL_STATE = {
contactRef: null,
locationRef: null,
findings: true,
};
export const ComponentContext = createContext(INITIAL_STATE);
const componentReducer = (state, action) => {
switch (action.type) {
case "contact":
return { contactRef: action.ref };
case "location":
return { locationRef: action.ref };
default:
return state;
}
};
export const ComponentProvider = (props) => {
const [state, dispatch] = useReducer(componentReducer, INITIAL_STATE);
return (
<ComponentContext.Provider
value={{
contactRef: state.contactRef,
locationRef: state.locationRef,
findings: state.findings,
dispatch,
}}
>
{props.children}
</ComponentContext.Provider>
);
};
Contact.js
import React, { useContext, useEffect, useRef } from "react";
import "./index.scss";
import { contactInfo } from "../../data/contactInfo";
import ContactPhoneIcon from "#mui/icons-material/ContactPhone";
import EmailIcon from "#mui/icons-material/Email";
import { ComponentContext } from "../../context/ComponentContext";
const Contact = () => {
const componentContext = useContext(ComponentContext);
const contactRef = useRef();
useEffect(() => {
componentContext.dispatch({ type: "contact", ref: contactRef });
}, []);
return (
<div className="contact" ref={contactRef}>
<div className="contact-accent"></div>
<div className="contact-body">
<div className="contact-left">
<h1 className="contact-title">Hit Me up!</h1>
<div className="contact-info">
<div className="contact-info-item">
<ContactPhoneIcon className="contact-info-icon" />
{contactInfo.phone}
</div>
<div className="contact-info-item">
<EmailIcon className="contact-info-icon" />
{contactInfo.email}
</div>
</div>
</div>
<div className="contact-right">
<p className="contact-description">
<b>I'm great with kids</b> <i>Sejarah</i> has been my passion since
high school and I'd love to show that to your kids; that history is
not just a boring compulsory subject for SPM.
</p>
</div>
</div>
</div>
);
};
export default Contact;
Header.js
import "./index.scss";
import React, { useContext } from "react";
import { ComponentContext } from "../../context/ComponentContext";
const Header = () => {
const componentContext = useContext(ComponentContext);
return (
<div className="header">
<div className="header-logo"></div>
<div className="header-sections">
<div className="header-section-item">Introduction</div>
<div className="header-section-item">About My Classes</div>
<div
className="header-section-item"
onClick={() => {
componentContext.contactRef.current.scrollIntoView({
behavior: "smooth",
});
}}
>
Contact Me
</div>
<div
className="header-section-item"
onClick={() => {
// componentContext.state.locationRef.current.scrollIntoView({
// behavior: "smooth",
// });
console.log(componentContext);
}}
>
Location
</div>
</div>
</div>
);
};
export default Header;
I'm starting using React Testing Library to make tests for a React Application, but i'm struggling to mock the data in a component that makes API calls using a Hook as service.
My component is just a functional component, with nothing extraordinary:
import React, { useEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import Skeleton from '#material-ui/lab/Skeleton'
import usePanelClient from '../../../clients/PanelClient/usePanelClient'
import useUtils from '../../../hooks/useUtils'
import IconFile from '../../../assets/img/panel/ico-file.svg'
import IconExclamation from '../../../assets/img/panel/ico-exclamation.svg'
import IconPause from '../../../assets/img/panel/awesome-pause-circle.svg'
import './PendingAwards.scss'
const PendingAwards = () => {
const pendingAwardsRef = useRef(null)
const location = useLocation()
const [loading, setLoading] = useState(true)
const [pendingAwards, setPendingAwards] = useState({})
const panelClient = usePanelClient()
const { formatCurrency } = useUtils()
useEffect(() => {
panelClient()
.getPendingAwards()
.then((response) => {
setLoading(false)
setPendingAwards(response.data)
})
}, [panelClient])
useEffect(() => {
const searchParams = new URLSearchParams(location.search)
if (searchParams.get('scrollTo') === 'pendingAwards') {
window.scrollTo({
top: 0,
behavior: 'smooth',
})
}
}, [location])
return (
<div
id="pendingAwards"
className="pending-awards-container"
ref={pendingAwardsRef}
>
<span className="pending-awards-container__title">Prêmios Pendentes</span>
{loading && (
<div className="skeleton-box">
<Skeleton width="100%" height="70px" />
<Skeleton width="100%" height="70px" />
</div>
)}
{!loading && (
<div className="pending-awards-values">
<div className="pending-awards-container__quantity">
<div className="pending-awards-container__quantity-container">
<span className="pending-awards-container__quantity-container-title">
Quantidade
</span>
<span className="pending-awards-container__quantity-container-content">
<div className="pending-awards-container__quantity-container-content__icon-box">
<img src={IconFile} alt="Ícone de arquivo" />
</div>
{pendingAwards.quantity ? pendingAwards.quantity : '0'}
</span>
</div>
</div>
<div className="pending-awards-container__amount">
<div className="pending-awards-container__amount-container">
<span className="pending-awards-container__amount-container-title">
Valor Pendente
</span>
<span className="pending-awards-container__amount-container-content">
<div className="pending-awards-container__amount-container-content__icon-box">
<img src={IconPause} alt="Ícone Pause" />
</div>
{pendingAwards.amount
? formatCurrency(pendingAwards.amount)
: 'R$ 0,00'}
</span>
</div>
</div>
<div className="pending-awards-container__commission">
<div className="pending-awards-container__commission-container">
<span className="pending-awards-container__commission-container-title">
Comissão Pendente
</span>
<span className="pending-awards-container__commission-container-content">
<div className="pending-awards-container__commission-container-content__icon-box">
<img src={IconExclamation} alt="Ícone exclamação" />
</div>
{pendingAwards.commission
? formatCurrency(pendingAwards.commission)
: 'R$ 0,00'}
</span>
</div>
</div>
</div>
)}
</div>
)
}
export default PendingAwards
My service that makes the API calls it is written like this:
import { useCallback } from 'react'
import axios from 'axios'
const usePanelClient = () => {
const getQuotationCard = useCallback(() => axios.get('/api/cards/quotation'), [])
const getCommissionCard = useCallback(() => axios.get('/api/cards/commission'), [])
const getPendingAwards = useCallback(() => axios.get('/api/premium/pending'), [])
return useCallback(() => ({
getQuotationCard,
getCommissionCard,
getPendingAwards,
}), [
getQuotationCard,
getCommissionCard,
getPendingAwards,
])
}
export default usePanelClient
In my current test I've tried mocking the hook like this, but I did not have success:
import React from 'react'
import { render } from '#testing-library/react'
import { Router } from 'react-router-dom'
import { createMemoryHistory } from 'history'
import PendingAwards from './PendingAwards'
describe('PendingAwards Component', () => {
beforeEach(() => {
jest.mock('../../../clients/PanelClient/usePanelClient', () => {
const mockData = {
quantity: 820,
amount: 26681086.12,
commission: 5528957.841628,
}
return {
getPendingAwards: jest.fn(() => Promise.resolve(mockData)),
}
})
})
it('should render the PendingAwards', () => {
const history = createMemoryHistory()
history.push = jest.fn()
const { container } = render(
<Router history={history}>
<PendingAwards />
</Router>,
)
expect(container).toBeInTheDocument()
})
it('should render the PendingAwards', () => {
const history = createMemoryHistory()
history.push({
search: '&scrollTo=pendingAwards',
})
window.scrollTo = jest.fn()
render(
<Router history={history}>
<PendingAwards />
</Router>,
)
expect(window.scrollTo).toHaveBeenCalledWith({ behavior: 'smooth', top: 0 })
})
})
May someone help me resolving this? I don't feel like this is something hard, but I've tried several things, and nothing seems to resolve it.
Thanks in advance.
You must call jest.mock at the top level of the module and for mocking ES6 modules with a default export you should use __esModule: true
jest.mock('../../../clients/PanelClient/usePanelClient', () => {
const mockData = {
quantity: 820,
amount: 26681086.12,
commission: 5528957.841628,
}
return {
__esModule: true,
default: ()=> ({
getPendingAwards: jest.fn(() => Promise.resolve({data: mockData})),
}),
}});
Two components talking to each other, when adding a product to the cart (component # 1), update the setState, via service (component # 2).
When adding the product to the cart, an error returns, saying that I don't have access to the export function of component # 1.
#1 Component
import React, { useContext, useEffect } from 'react';
import Link from 'next/link';
import { Clipboard } from 'react-feather';
import { OrderCartButton } from './styles';
import OrderService from '~/services/orderservice';
import Context from '~/utils/context';
function OrderCart() {
const { state, actions } = useContext(Context);
function updateQty() {
const qty = OrderService.count();
actions({ type: 'setState', payload: { ...state, value: qty } });
}
useEffect(() => {
updateQty();
}, []);
return (
<>
<Link href="/order">
<OrderCartButton data-order-qty={state.value}>
<Clipboard />
</OrderCartButton>
</Link>
</>
);
}
export default OrderCart;
#2 Component
import { reactLocalStorage } from 'reactjs-localstorage';
import { toast } from 'react-toastify';
class OrderService {
async add(product) {
const oldorder = reactLocalStorage.getObject('_order_meumenu');
if (oldorder.length) {
const merged = [...oldorder, ...product].reduce(
(r, { id, qty, title, description, price, image }) => {
const item = r.find((q) => q.id === id);
if (item) item.qty += qty;
else r.push({ id, qty, title, description, price, image });
return r;
},
[]
);
await reactLocalStorage.setObject('_order_meumenu', merged);
} else {
await reactLocalStorage.setObject('_order_meumenu', product);
}
toast.success('Produto adicionado ao Pedido');
const qty = await this.count();
return qty;
},
async count() {
const order = reactLocalStorage.getObject('_order_meumenu');
return order.length || 0;
},
}
export default OrderService;
Component #3 - Context Moved to callback
import React, { useState, useContext } from 'react';
import { Plus, Minus } from 'react-feather';
import { ProductContainer } from './styles';
import currency from '../../utils/currency';
import OrderService from '~/services/orderservice';
import Context from '~/utils/context';
function Product(product) {
const { state, actions } = useContext(Context);
const [qty, setQty] = useState(1);
function addProductOrder(elem, elemQty) {
// eslint-disable-next-line no-param-reassign
const newElem = [];
newElem.push({ ...elem, qty: elemQty });
OrderService.add(newElem).then((val) =>
actions({ type: 'setState', payload: { ...state, value: val } })
);
}
return (
<ProductContainer>
<div className="product-image">
<img
className="image"
src={product.image}
alt={product.title}
data-product-image
/>
</div>
<div className="product-details">
<div className="product-top">
<div className="product-title">
<span className="title" data-product-title>
{product.title}
</span>
<span className="desc" data-product-desc>
{product.description}
</span>
</div>
<button
type="button"
className="product-add"
onClick={() => addProductOrder(product, qty)}
>
<span className="btn -icon -rounded" title="Add Produto">
<Plus className="icon" />
</span>
</button>
</div>
<div className="product-bottom">
<div className="product-qty">
<div className="product-control-number">
<Minus className="icon" onClick={() => setQty(qty - 1)} />
<input
className="input"
type="number"
min="1"
max="9"
value={qty}
readOnly
data-number-value
/>
<Plus className="icon" onClick={() => setQty(qty + 1)} />
</div>
</div>
<div
className="product-price"
data-product-price={product.price}
data-product-totalprice="9"
>
{currency(product.price)}
</div>
</div>
</div>
</ProductContainer>
);
}
export default Product;
There are a few things to fix:
import OrderCart from '~/components/OrderCart';
// ...
OrderCart.updateQty();
the default export in ~/components/OrderCart is the class component(OrderCart) and updateQty is another function in the same file, so the import statement should be something like:
import { updateQty } from '~/components/OrderCart';
and the usage should be
updateQty()
but this will not work because calling a function that returns some object will not cause a rerender.
So, to fix this you should pass a callback to the child component that calls the add function, and call the callback after invoking add.
The callback function to pass as props to the child can be handleUpdateQty.