How do write unit test for function inside Context API? - reactjs

Currently I am not able to test the lines as test coverage says. Please help me.
Test coverage is saying lines like dispatch but I don't know how to test it.
This is my file ListMeetingContext.tsx
import React, { createContext, ReactElement, useReducer } from 'react';
import { IMeeting } from '#interfaces';
import { REDUCER_ACTIONS } from '#enums/ReducerAction';
import { meetingData } from '#constants';
export const initListMeeting: IMeeting[] = meetingData;
type ReducerAction = {
type: REDUCER_ACTIONS;
payload: IMeeting | number;
};
export type MeetingsContext = {
children: ReactElement | ReactElement[];
};
export type ListMeetingContextType = {
listMeeting: IMeeting[];
handleAddMeeting: (newMeeting: IMeeting) => void;
handleUpdateMeeting: (newMeeting: IMeeting) => void;
handleRemoveMeeting: (id: number) => void;
};
export const reducer = (
state: IMeeting[],
action: ReducerAction,
): IMeeting[] => {
switch (action.type) {
case REDUCER_ACTIONS.ADD_MEETING:
// TODO: as IMeeting
return [...state, action.payload as IMeeting];
case REDUCER_ACTIONS.REMOVE_MEETING:
return state.filter((meeting) => meeting.id !== action.payload);
case REDUCER_ACTIONS.UPDATE_MEETING:
return state.map((meeting: IMeeting) =>
// TODO: as IMeeting
meeting.id === (action.payload as IMeeting).id
? (action.payload as IMeeting)
: meeting,
);
default:
return state;
}
};
export const ListMeetingContext = createContext<ListMeetingContextType>({
listMeeting: meetingData,
handleAddMeeting: () => null,
handleUpdateMeeting: () => null,
handleRemoveMeeting: () => null,
});
export const StateProvider = ({ children }: MeetingsContext): JSX.Element => {
const [listMeeting, dispatch] = useReducer(reducer, initListMeeting);
const handleAddMeeting = (newMeeting: IMeeting) => {
// Uncovered line
dispatch({ type: REDUCER_ACTIONS.ADD_MEETING, payload: newMeeting });
};
const handleUpdateMeeting = (newMeeting: IMeeting) => {
// Uncovered line
dispatch({ type: REDUCER_ACTIONS.UPDATE_MEETING, payload: newMeeting });
};
const handleRemoveMeeting = (id: number) => {
// Uncovered line
dispatch({ type: REDUCER_ACTIONS.REMOVE_MEETING, payload: id });
};
return (
<ListMeetingContext.Provider
value={{
listMeeting,
handleAddMeeting,
handleRemoveMeeting,
handleUpdateMeeting,
}}
>
{children}
</ListMeetingContext.Provider>
);
};
This is Image about test Coverage
The test for coverage lines tells me to put it at the bottom
This is file ListMeetingContext.test.tsx
import React from 'react';
import {
ListMeetingContext,
MeetingsContext,
reducer,
StateProvider,
} from '#context';
import { REDUCER_ACTIONS } from '#enums';
import { IMeeting } from '#interfaces';
import renderer from 'react-test-renderer';
import { meetingData } from '#constants';
import { fireEvent, render, screen } from '#testing-library/react';
describe('Test [ListMeetingContext] function', () => {
test('Action [ADD_MEETING] should update context state/add new item to context state with action[ADD_MEETING]', () => {
const state: IMeeting[] = [];
const action = {
type: REDUCER_ACTIONS.ADD_MEETING,
payload: meetingData[0],
};
const result = reducer(state, action);
expect(result).toEqual([action.payload]);
});
test('Action [REMOVE_MEETING] should update context state/remove an item from context state with action[REMOVE_MEETING]', () => {
const state: IMeeting[] = meetingData;
const action = {
type: REDUCER_ACTIONS.REMOVE_MEETING,
payload: 1,
};
const result = reducer(state, action);
expect(result).toEqual([meetingData[1]]);
});
test('Action [UPDATE_MEETING] should update context state/update an item from context state with action[UPDATE_MEETING]', () => {
const state: IMeeting[] = [meetingData[0]];
const action = {
type: REDUCER_ACTIONS.UPDATE_MEETING,
payload: meetingData[0],
};
const result = reducer(state, action);
expect(result).toEqual([action.payload]);
});
test('Action [EMPTY_ACTION] should return state', () => {
const state: IMeeting[] = [meetingData[0]];
const action = {
type: REDUCER_ACTIONS.EMPTY_ACTION,
payload: meetingData[0],
};
const result = reducer(state, action);
expect(result).toEqual(state);
});
test('[StateProvider] with default value', () => {
const component = renderer
.create(<StateProvider {...({} as MeetingsContext)} />)
.toJSON();
expect(component).toMatchSnapshot();
});
type Props = {
handleAddItem: () => void;
handleUpdateItem: () => void;
handleRemoveItem: () => void;
};
const TestComponent = ({
handleAddItem,
handleUpdateItem,
handleRemoveItem,
}: Props): JSX.Element => {
return (
<>
<button data-testid="remove-meeting-button" onClick={handleRemoveItem}>
Remove
</button>
<button data-testid="add-meeting-button" onClick={handleAddItem}>
Add
</button>
<button data-testid="update-meeting-button" onClick={handleUpdateItem}>
Update
</button>
</>
);
};
test('component handles button click', () => {
const handleAddMeeting = jest.fn();
const handleUpdateMeeting = jest.fn();
const handleRemoveMeeting = jest.fn();
const listMeeting = meetingData;
render(
<ListMeetingContext.Provider
value={{
listMeeting,
handleAddMeeting,
handleUpdateMeeting,
handleRemoveMeeting,
}}
>
<TestComponent
handleAddItem={handleAddMeeting}
handleUpdateItem={handleUpdateMeeting}
handleRemoveItem={handleRemoveMeeting}
/>
</ListMeetingContext.Provider>,
);
fireEvent.click(screen.getByTestId('remove-meeting-button'));
expect(handleRemoveMeeting).toHaveBeenCalledTimes(1);
});
});
Currently I am not able to test the lines as test coverage says. Please help me

Related

when I use useDispatch inside useEffect my component keep rendering

When I use dispatch as follows in my react component, My component keeps rendering. How can I avoid that?
const dispatch = useDispatch();
useEffect(() => {
dispatch(reportsActionCreators.changeSalesDashboardData(someData));
}, []);
in the parent component, I'm using useSelector as this. But didn't use this report's data.
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
this is the parent component I'm using.
const SalesReports: FC = () => {
const dispatch = useDispatch();
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
});
const getPageContent = useMemo(() => {
switch (selectedSalesTab) {
case salesReportsTabs[0].id:
return <Dashboard />;
default:
return <div>not found :(</div>;
}
}, [selectedSalesTab]);
return (
<div className="sales-report-wrapper">
<GTTabs
id="sales-reports-tabs"
onClickTab={(tab: Tab) => dispatch(reportsActionCreators.changeSalesTab(tab.id))}
tabs={salesReportsTabs}
defaultSelectedTabId={selectedSalesTab}
/>
<div>{getPageContent}</div>
</div>
);
};
export default SalesReports;
this is the Child component I'm using
const Dashboard: FC = () => {
const repostsRxjs = rxjsConfig(reportingAxios);
const dispatch = useDispatch();
useEffect(() => {
repostsRxjs
.post<SalesDashboardItem[]>(
'/sales-data/order-details/6087bc3606ff073930a10848?timezone=Asia/Dubai&from=2022-09-03T00:00:00.00Z&to=2022-12-25T00:00:00.00Z&size=10',
{
brandIds: [],
channelIds: [],
kitchenIds: [],
countryIds: [],
},
)
.pipe(
take(1),
catchError((err: any) => of(console.log(err))),
)
.subscribe((response: SalesDashboardItem[] | void) => {
if (response) {
dispatch(reportsActionCreators.changeSalesDashboardData(response));
}
});
}, []);
const { isActiveFilter } = useSelector<RootState, any>((state: RootState) => {
return {
isActiveFilter: state.filterData.isActiveFilter,
};
});
return (
<>
<div
onClick={() => {
dispatch(filterssActionCreators.handleFilterPanel(!isActiveFilter));
dispatch(
filterssActionCreators.changeSelectedFiltersType([
FilterTypes.BRAND,
FilterTypes.CHANNEL,
FilterTypes.COUNTRY,
FilterTypes.KITCHEN,
]),
);
}}
>
Dashboard
</div>
{isActiveFilter && <FilterPanel />}
</>
);
};
export default Dashboard;
Actions
import { SalesDashboardItem } from 'app/models/Reports';
import { actionCreator } from 'app/state/common';
export type ChangeSalesTabPayload = string;
export type ChangeSalesDashboardDataPayload = SalesDashboardItem[];
export const reportsActionTypes = {
CHANGE_SALES_TAB: 'CHANGE_SALES_TAB',
CHANGE_SALES_DASHABOARD_DATA: 'CHANGE_SALES_DASHABOARD_DATA',
};
export const reportsActionCreators = {
changeSalesTab: actionCreator<ChangeSalesTabPayload>(reportsActionTypes.CHANGE_SALES_TAB),
changeSalesDashboardData: actionCreator<ChangeSalesDashboardDataPayload>(
reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA,
),
};
export type ReportsAction = {
type: typeof reportsActionTypes.CHANGE_SALES_TAB | typeof reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA;
payload: ChangeSalesTabPayload | ChangeSalesDashboardDataPayload;
};
Reducer
import { SalesDashboardItem } from 'app/models/Reports';
import { salesReportsTabs } from 'app/utils/reports';
import { reportsActionTypes, ReportsAction } from './actions';
export type ReportsState = {
selectedSalesTab: string;
salesDashboardFilterData: {
brands: string[];
kitchens: string[];
channels: string[];
countries: string[];
};
salesDashBoardDatta: SalesDashboardItem[];
};
const initialState: ReportsState = {
selectedSalesTab: salesReportsTabs[0].id,
salesDashboardFilterData: {
brands: [],
kitchens: [],
channels: [],
countries: [],
},
salesDashBoardDatta: [],
};
export default (state = initialState, action: ReportsAction): ReportsState => {
switch (action.type) {
case reportsActionTypes.CHANGE_SALES_TAB:
return { ...state, selectedSalesTab: action.payload as string };
case reportsActionTypes.CHANGE_SALES_DASHABOARD_DATA:
return { ...state, salesDashBoardDatta: action.payload as SalesDashboardItem[] };
default:
return state;
}
};
root reducer
import { combineReducers } from 'redux';
import SidePanelReducer from './reducers/sidepanel.reducer';
import authReducer from './auth';
import onboardingReducer from './onboarding';
import applicationReducer from './application';
import inventoryConfigReducer from './inventoryConfig/inventory.reducer';
import reportsReducer from './reports';
import filtersReducer from './filter';
const rootReducer = combineReducers({
sidePanel: SidePanelReducer,
auth: authReducer,
onboarding: onboardingReducer,
application: applicationReducer,
inventory: inventoryConfigReducer,
reports: reportsReducer,
filterData: filtersReducer,
});
export default rootReducer;
when I'm adding the dispatch action in useEffect(componentDidMount) this looping is happening. Otherwise, this code works fine. How can I avoid that component rerendering?
I think the issue is that the useSelector hook is returning a new object reference each time which triggers the useMemo hook to re-memoize an "instance" of the Dashboard component. The new "instance" of Dashboard then mounts and runs its useEffect hook which dispatches an action that updates the state.reports state in the Redux store.
Instead of creating and returning a new object reference to destructure selectedSalesTab from, just return the state.reports object directly.
Change
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return {
selectedSalesTab: state.reports.selectedSalesTab,
};
});
to
const { selectedSalesTab } = useSelector<RootState, any>((state: RootState) => {
return state.reports;
});

Cannot read properties of undefined (the general state is undefined)

I just finished a small project and I tried to publish it on Vercel, the project works without errors locally, but on the server I get the error: "Cannot read properties of undefined (reading 'notes')", apparently not It recognizes the state that I have in my application, this would be my useNotes file:
import { useContext } from "react";
import { NotesContext } from "../context/NotesContext";
export const useNotes = () => {
const { notestate, toggleNote, addNote, changeState, handleDeleteNote } = useContext(NotesContext);
const { notes } = notestate;
return {
noteGeneral: notestate,
notes: notes,
notinteresting: notes?.filter(note => !note?.interesting).length,
interesting: notes?.filter(note => note?.interesting === true).length,
lisInteresting: notes?.filter(note => note?.interesting === true),
listNotInteresting: notes?.filter(note => note?.interesting === false),
toggleNote,
addNote,
changeState,
handleDeleteNote
}
}
this would be the context of my application:
import { createContext } from "react";
import { Note, NoteState } from "../interfaces/interfaces";
export type NoteContextProps = {
notestate: NoteState,
toggleNote: (id: number) => void;
addNote: (note: Note) => void;
changeState: (action: string) => void;
handleDeleteNote: (id: number) => void;
}
export const NotesContext = createContext<NoteContextProps>({} as NoteContextProps);
and this is the provider of my application
import { useEffect, useReducer } from 'react';
import { Note, NoteState } from '../interfaces/interfaces';
import { NoteReducer } from './NoteReducer';
import { NotesContext } from './NotesContext';
import { DateNote } from '../../helpers/DateNote';
const { today } = DateNote();
const INITIAL_STATE: NoteState = {
notesCount: 2,
notes: [
{
id: 1,
description: 'Welcome to NotesApp, here you can write all the notes you want. If you liked the project leave a comment on my social networks :)',
interesting: true,
title: 'Hello there',
created: today
},
],
active: 'All',
}
interface props {
children: JSX.Element | JSX.Element[]
}
const NotesProvider = ({ children }: props) => {
const localData = localStorage.getItem('notes');
const toggleNote = (id: number) => {
dispatch({ type: 'toggleInteresting', payload: { id } })
}
const addNote = (note: Note) => {
dispatch({ type: 'addNote', payload: note })
}
const changeState = (active: string) => {
dispatch({ type: 'changeState', payload: active })
}
const handleDeleteNote = (id: number) => {
dispatch({ type: 'deleteNote', payload: { id } })
}
const [notestate, dispatch] = useReducer(NoteReducer, INITIAL_STATE, () => {
return localData ? JSON.parse(localData) : localStorage.setItem('notes', JSON.stringify(INITIAL_STATE))
});
useEffect(() => {
localStorage.setItem('notes', JSON.stringify(notestate))
}, [notestate])
return (
<NotesContext.Provider value={{ toggleNote, addNote, changeState, handleDeleteNote, notestate }}>
{children}
</NotesContext.Provider>
)
}
export default NotesProvider;
I am storing the state of my application in Localstorage.
I've been looking for the solution for more than two days but I can't find it
const [notestate, dispatch] = useReducer(NoteReducer, INITIAL_STATE, () => {
return localData ? JSON.parse(localData) : localStorage.setItem('notes', JSON.stringify(INITIAL_STATE))
});
I think here when there is no local data, the notestate receives value from localStorage.setItem(), which is undefined, so the notestate will be undefined.

Using `useContext` with TypeScript - Type 'MyType | null` is not assignable to type 'MyType'

I'm working on implementing TypeScript on a small codebase I've been working on and having a few troubles with the above error. I've searched for answers but none seemed to fix the actual issue I was having.
I'm getting the error:
Type 'ContextType | null' is not assignable to type 'ContextType'.
I have tried setting it to null, undefined, and object, and nothing seems to help so I'd appreciate some help with this one!
Store.txt
import { useReducer, createContext, useMemo } from "react";
import { INITIAL_DATA } from './todo/constants';
import { ContextType, ITodo, ACTIONTYPE } from './todo/models';
export const StoreContext = createContext<ContextType | null>(null)
export enum ACTIONS {
DELETE_TODO = "delete_todo",
ADD_TODO = "add_todo",
};
const reducer = (state: ITodo, action: ACTIONTYPE) => {
switch (action.type) {
case ACTIONS.DELETE_TODO:
return [...action.payload];
case ACTIONS.ADD_TODO:
return action.payload;
default:
return state;
}
};
interface Props {
children: React.ReactNode;
}
export const StoreProvider = ({ children }: Props) => {
const [state, dispatch] = useReducer(reducer, INITIAL_DATA);
const contextValue: ContextType = useMemo(() => {
return { state, dispatch };
}, [state, dispatch]);
return (
<StoreContext.Provider value={contextValue}>
{children}
</StoreContext.Provider>
);
};
Component.tsx
import { useContext } from 'react';
import { StoreContext, ACTIONS } from '../../../store';
import { ContextType } from '../../models'
export const AddTodo = () => {
const { state, dispatch }: ContextType = useContext(StoreContext);
const validateFirstStep = async () => {
return await isValid(firstStepForm);
}
const closeDrawer = () => {
onClose();
}
const handleSubmit = async () => {
const newTodoEntry = { /** todo **/ }
const newData = [...state, newTodoEntry];
dispatch({ type: ACTIONS.ADD_TODO, payload: newData });
}
return (
<div>
{ /* Something Todo happens here */ }
</div>
)
}
Models.tsx
import { ACTIONS } from '../../store';
export type ITodos = {
id: string;
todoName: string;
todoType: string;
}[];
export type ContextType = {
state: ITodos;
dispatch: React.Dispatch<ACTIONTYPE>;
}
export type ACTIONTYPE =
| { type: ACTIONS.ADD_TODO, payload: ITodos }
| { type: ACTIONS.DELETE_TODO; payload: ITodos }
You need to provide default context value in case of there is no provider higher in the react tree;
export const StoreContext = createContext<ContextType>({todos: [], dispatch: () => {}})

Error: Invalid hook call. Hooks can only be called inside of the body of a function component - When adding Loader component

I'm working a Nextjs - Typescript project. I'am trying to add a Loader component. while page is loading loader component is true. otherwise false.
This is my code:
LoderContext.ts
import React, { useReducer } from "react";
import { NextPage } from "next";
type State = { loading: boolean };
type Action = { type: "startLoading" } | { type: "endLoading" };
type Dispatch = (action: Action) => void;
export const LoadingContext =
React.createContext<{ state: State; dispatch: Dispatch } | undefined>(
undefined
);
const initialState = {
loading: true,
};
const { Provider } = LoadingContext;
const reducer = (state: State, action: Action) => {
switch (action.type) {
case "startLoading":
return {
loading: true,
};
case "endLoading":
return {
loading: false,
};
default:
throw state;
}
};
const LoadingProvider: NextPage = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const value = { state, dispatch };
return <Provider value={value}>{children}</Provider>;
};
export function useLoading() {
const context = React.useContext(LoadingContext); // error is somewhere here
if (context === undefined) {
throw new Error("useCount must be used within a CountProvider");
}
return context;
}
export default LoadingProvider;
Loder.tsx
const Loader: NextPage = () => {
return (
<div className={styles.loader_wrapper}>
<div className={styles.content}>
<span className={styles.loading_text}>Loading...</span>
</div>
</div>
);
};
export default Loader;
Index.tsx
import Loader from "../components/Loader/Loader";
import { useLoading } from "../context/LoadingContext";
import { useRouter } from "next/dist/client/router";
const { state, dispatch } = useLoading();
const router = useRouter();
useEffect(() => {
router.events.on("routeChangeStart", () => {
dispatch({ type: "startLoading" });
});
return () => {
router.events.off("routeChangeStart", () => {
dispatch({ type: "endLoading" });
});
};
}, [dispatch, router]);
useEffect(() => {
dispatch({ type: "endLoading" });
}, [dispatch, router]);
const Home: NextPage = () => {
return (
<>
{state.loading ? (
<Loader />
) : (
<>
<div>
// other components
</div>
</>
)}
</>
);
};
I got this error. Error: Invalid hook call. Hooks can only be called inside of the body of a function component
What are the mistakes in my code?
Your index page is calling useLoading, useEffect and useRouter outside the Home component.
Refer react documentation. It clearly mentions the issue you're facing.
Hooks can only be called inside the body of a function component.
It should be something like this.
import Loader from "../components/Loader/Loader";
import { useLoading } from "../context/LoadingContext";
import { useRouter } from "next/dist/client/router";
const Home: NextPage = () => {
const { state, dispatch } = useLoading();
const router = useRouter();
useEffect(() => {
router.events.on("routeChangeStart", () => {
dispatch({ type: "startLoading" });
});
return () => {
router.events.off("routeChangeStart", () => {
dispatch({ type: "endLoading" });
});
};
}, [dispatch, router]);
useEffect(() => {
dispatch({ type: "endLoading" });
}, [dispatch, router]);
return (
<>
{state.loading ? (
<Loader />
) : (
<>
<div>
// other components
</div>
</>
)}
</>
);
};

Property 'filter' does not exist on type object

I'm working with zustand and typescript. I got this error on my code
Property 'filter' does not exist on type 'object'.ts(2339)
I don't know how to fix this problem.
I'm creating an object from my useStore library.
I tried usin 'type' but it throws these errors on the code editor
error
TS
import React from "react";
import "./App.css";
import create from "zustand";
type PkState = {
filter: "",
pokemon: [],
}
const useStore = create((PkState) => ({
PkStateFilter: (filter) =>
PkState((state) => ({
...state,
filter,
})),
PkStatePokemon: (pokemon) =>
PkState((state) => ({
...state,
pokemon,
})),
}));
// input
const FilterInput = () => {
const filter = useStore((state) => state.filter);
const PkStateFilter = useStore((state) => state.PkStateFilter);
return ( <input value={filter} onChange=((evt)=>PkStateFilter(evt.target.value)) />
)
};
function App() {
return <div className="App"></div>;
}
export default App;
You need to define a type for your store.
import React from "react";
import "./App.css";
import create from "zustand";
type State = {
filter: string;
pokemon: Array<string>;
setFilter: (filter: string) => void;
setPokemon: (pokemon: Array<string>) => void;
};
// You have to use the type here
const useStore = create<State>((set) => ({
filter: "", // set initial values here
pokemon: [],
setFilter: (filter) =>
set((state) => ({
...state,
filter
})),
setPokemon: (pokemon) =>
set((state) => ({
...state,
pokemon
}))
}));
// input
const FilterInput = () => {
const filter = useStore((state) => state.filter);
const setFilter = useStore((state) => state.setFilter);
return (
<input value={filter} onChange={(evt) => setFilter(evt.target.value)} />
);
};
function App() {
return (
<div className="App">
</div>
);
}
export default App;
I am not sure this will work, try with distracting filter on the next way:
const { filter } = useStore((state) => state);
Also you can debug useStore with console.log, to check what returning:
const returnedState = useStore((state) => state);
// Check does `filter` exists in state
console.warn('returnedState', returnedState);

Resources