hope someone can point me the right direction with this. Basically I've created a react app which makes use of hooks, specifically useContext, useEffect and useReducer. My problem is that I can't seem to get tests to detect click or dispatch events of the related component.
Stripped down version of my app can be found at : https://github.com/atlantisstorm/hooks-testing
Tests relate to layout.test.js script.
I've tried various approaches, different ways of mocking dispatch, useContext, etc but no joy with it. Most recent version.
layout.test.js
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import App from './app';
import { Provider, initialState } from './context';
const dispatch = jest.fn();
const spy = jest
.spyOn(React, 'useContext')
.mockImplementation(() => ({
state: initialState,
dispatch: dispatch
}));
describe('Layout component', () => {
it('starts with a count of 0', () => {
const { getByTestId } = render(
<App>
<Provider>
<Layout />
</Provider>
</App>
);
expect(dispatch).toHaveBeenCalledTimes(1);
const refreshButton = getByTestId('fetch-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(3);
});
});
layout.jsx
import React, { useContext, useEffect } from 'react';
import { Context } from "./context";
const Layout = () => {
const { state, dispatch } = useContext(Context);
const { displayThings, things } = state;
const onClickDisplay = (event) => {
// eslint-disable-next-line
event.preventDefault;
dispatch({ type: "DISPLAY_THINGS" });
};
useEffect(() => {
dispatch({ type: "FETCH_THINGS" });
}, [displayThings]);
const btnText = displayThings ? "hide things" : "display things";
return (
<div>
<button data-testid="fetch-button" onClick={onClickDisplay}>{btnText}</button>
{ displayThings ?
<p>We got some things!</p>
:
<p>No things to show!</p>
}
{ displayThings && things.map((thing) =>
<p>{ thing }</p>
)}
</div>
)
}
export default Layout;
app.jsx
import React from 'react';
import Provider from "./context";
import Layout from './layout';
const App = () => {
return (
<Provider>
<Layout />
</Provider>
)
}
export default App;
context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
reducer.jsx
export const reducer = (state, action) => {
switch (action.type) {
case "DISPLAY_THINGS": {
const displayThings = state.displayThings ? false : true;
return { ...state, displayThings };
}
case "FETCH_THINGS": {
const things = state.displayThings ? [
"thing one",
"thing two"
] : [];
return { ...state, things };
}
default: {
return state;
}
}
};
I'm sure the answer will be easy when I see it, but just trying to figure out I can detect the click event plus detect the 'dispatch' events? (I've already got separate test in the main app to properly test dispatch response/actions)
Thank you in advance.
EDIT
Ok, I think I've got a reasonable, though not perfect, solution. First I just added optional testDispatch and testState props to the context.jsx module.
new context.jsx
import React, { createContext, useReducer } from "react";
import { reducer } from "./reducer";
export const Context = createContext();
export const initialState = {
displayThings: false,
things: []
};
export const Provider = ({ children, testDispatch, testState }) => {
const [iState, iDispatch] = useReducer(reducer, initialState);
const dispatch = testDispatch ? testDispatch : iDispatch;
const state = testState ? testState : iState;
return (
<Context.Provider value={{ state, dispatch }}>
{children}
</Context.Provider>
);
};
export default Provider;
Then in layout.test.jsx I just simply pass in mocked jest dispatch function plus state as necessary. Also removed the outer App wrapping as that seemed to prevent the props from being passed through.
new layout.test.jsx
import React from 'react';
import { render, fireEvent } from "#testing-library/react";
import Layout from './layout';
import { Provider } from './context';
describe('Layout component', () => {
it('starts with a count of 0', () => {
const dispatch = jest.fn();
const state = {
displayThings: false,
things: []
};
const { getByTestId } = render(
<Provider testDispatch={dispatch} testState={state}>
<Layout />
</Provider>
);
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenNthCalledWith(1, { type: "FETCH_THINGS" });
const refreshButton = getByTestId('fetch-things-button');
fireEvent.click(refreshButton);
expect(dispatch).toHaveBeenCalledTimes(2);
// Important: The order of the calls should be this, but dispatch is reporting them
// the opposite way around in the this test, i.e. FETCH_THINGS, then DISPLAY_THINGS...
//expect(dispatch).toHaveBeenNthCalledWith(1, { type: "DISPLAY_THINGS" });
//expect(dispatch).toHaveBeenNthCalledWith(2, { type: "FETCH_THINGS" });
// ... so as dispatch behaves correctly outside of testing for the moment I'm just settling for
// knowing that dispatch was at least called twice with the correct parameters.
expect(dispatch).toHaveBeenCalledWith({ type: "DISPLAY_THINGS" });
expect(dispatch).toHaveBeenCalledWith({ type: "FETCH_THINGS" });
});
});
One little caveat though, as noted above, when the 'fetch-things-button' was fired, it reported the dispatch in the wrong order. :/ So I just settled for knowing the correct calls where triggered, but if anyone knows why the call order isn't as expected I would be pleased to know.
https://github.com/atlantisstorm/hooks-testing update to reflect the above if anyone is interested.
A few months back I was also trying to write unit tests for reducer + context for an app. So, here's my solution to test useReducer and useContext.
FeaturesProvider.js
import React, { createContext, useContext, useReducer } from 'react';
import { featuresInitialState, featuresReducer } from '../reducers/featuresReducer';
export const FeatureContext = createContext();
const FeaturesProvider = ({ children }) => {
const [state, dispatch] = useReducer(featuresReducer, featuresInitialState);
return <FeatureContext.Provider value={{ state, dispatch }}>{children}</FeatureContext.Provider>;
};
export const useFeature = () => useContext(FeatureContext);
export default FeaturesProvider;
FeaturesProvider.test.js
import React from 'react';
import { render } from '#testing-library/react';
import { renderHook } from '#testing-library/react-hooks';
import FeaturesProvider, { useFeature, FeatureContext } from './FeaturesProvider';
const state = { features: [] };
const dispatch = jest.fn();
const wrapper = ({ children }) => (
<FeatureContext.Provider value={{ state, dispatch }}>
{children}
</FeatureContext.Provider>
);
const mockUseContext = jest.fn().mockImplementation(() => ({ state, dispatch }));
React.useContext = mockUseContext;
describe('useFeature test', () => {
test('should return present feature toggles with its state and dispatch function', () => {
render(<FeaturesProvider />);
const { result } = renderHook(() => useFeature(), { wrapper });
expect(result.current.state.features.length).toBe(0);
expect(result.current).toEqual({ state, dispatch });
});
});
featuresReducer.js
import ApplicationConfig from '../config/app-config';
import actions from './actions';
export const featuresInitialState = {
features: [],
environments: ApplicationConfig.ENVIRONMENTS,
toastMessage: null
};
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
export const featuresReducer = (state, { type, payload }) => {
switch (type) {
case INITIALIZE_DATA:
return {
...state,
[payload.name]: payload.data
};
case TOGGLE_FEATURE:
return {
...state,
features: state.features.map((feature) => (feature.featureToggleName === payload.featureToggleName
? {
...feature,
environmentState:
{ ...feature.environmentState, [payload.environment]: !feature.environmentState[payload.environment] }
}
: feature))
};
case ENABLE_OR_DISABLE_TOAST:
return { ...state, toastMessage: payload.message };
default:
return { ...state };
}
};
featuresReducer.test.js
import { featuresReducer } from './featuresReducer';
import actions from './actions';
const { INITIALIZE_DATA, TOGGLE_FEATURE, ENABLE_OR_DISABLE_TOAST } = actions;
describe('Reducer function test', () => {
test('should initialize data when INITIALIZE_DATA action is dispatched', () => {
const featuresState = {
features: []
};
const action = {
type: INITIALIZE_DATA,
payload: {
name: 'features',
data: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
}
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should toggle the feature for the given feature and environemt when TOGGLE_FEATURE action is disptched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
};
const action = {
type: TOGGLE_FEATURE,
payload: { featureToggleName: '23456_WOPhotoDownload', environment: 'sit' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}, {
featureId: '23458', featureName: 'WO photo download', featureToggleName: '23458_WOPhotoDownload', environmentState: { sit: true, replica: true, prod: false }
}]
});
});
test('should enable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with the message as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: 'Something went wrong!' }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: 'Something went wrong!' });
});
test('should disable the toast message when ENABLE_OR_DISABLE_TOAST action is dispatched with message as null as part of payload', () => {
const featuresState = {
toastMessage: null
};
const action = {
type: ENABLE_OR_DISABLE_TOAST,
payload: { message: null }
};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({ toastMessage: null });
});
test('should return the current state when the action with no specific type is dispatched', () => {
const featuresState = {
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
};
const action = {};
const updatedState = featuresReducer(featuresState, action);
expect(updatedState).toEqual({
features: [{
featureId: '23456', featureName: 'WO photo download', featureToggleName: '23456_WOPhotoDownload', environmentState: { sit: false, replica: true, prod: false }
}]
});
});
});
Related
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;
});
I've searched high and low for best practices when using testing react library. I have a test which uses react context which I'm trying to test with a component to ensure it updates correct. It's quite a bit of code so I'll narrow it down here (though still a lot).
Working version of this code can be found here https://codesandbox.io/s/react-playground-forked-mllwv8 (though the test platform isn't set up so this may not help much as not sure how to implement webpack on cs)
The run of functionality is:
PeoplePageComponent loads
useEffect runs and runs fetchPeople from the CONTEXT
fetchPeople(context function) runs fetchPeople(actual function) from fetchPeople.jsx which dispatches SET_PEOPLE
in PeoplePageComponent useEffect is triggered to where logic setsCurrentPage
I want to test:
render PeoplePageComponent with providerValue (people: null)
Assert page is loading
assert mockFetchPeople was called 1 time
Rerender PeoplePageComponent with providerValue (people:[{name: "tommy", age: 24}]) (THIS IS THE BIT I'M UNSURE OF)
assert mockSetCurrentPage was called 1 time
assert page is hasPeople
What would be the best way to do this given the set up I have?
please bare in mind this code is actually very different to the code I'm using but the mechanisms are used in the same way
fetchPeople.jsx
export const fetchPeople = (dispatch) => {
// faking a request here for the sake of demonstration
const people = [
{
name: "Tommy",
age: 24
}
];
setTimeout(() => {
dispatch({
type: "SET_PEOPLE",
payload: people
});
}, 5000);
};
PeoplePageComponent.jsx
import React, { useContext, useEffect } from "react";
import { MyContext } from "./MyProvider";
export const PeoplePageComponent = () => {
const { currentPage, people, setPage, fetchPeople } = useContext(MyContext);
useEffect(() => {
if (!people) return;
setPage(people.length > 0 ? "hasPeople" : "noPeople");
}, [people]);
useEffect(() => {
fetchPeople();
}, []);
return (
<>
<p>
<b>Page:</b> {currentPage}
</p>
<p>
<b>People:</b> {JSON.stringify(people)}
</p>
{currentPage === "loading" && <h1>This is the loading page</h1>}
{currentPage === "noPeople" && <h1>This is the noPeople page</h1>}
{currentPage === "hasPeople" && <h1>This is the hasPeople page</h1>}
</>
);
};
MyProvider.jsx
import { createContext, useReducer } from "react";
import { fetchPeople } from "./fetchPeople";
const reducer = (state, action) => {
switch (action.type) {
case "SET_PAGE":
return {
...state,
currentPage: action.payload
};
case "SET_PEOPLE":
return {
...state,
people: action.payload
};
default:
return state;
}
};
const initialState = {
currentPage: "loading",
people: null
};
export const MyContext = createContext({
setPage: () => {},
setPeople: () => {},
fetchPeople: () => {},
currentPage: "loading",
people: null
});
export const MyProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const providerValue = {
setPage: (page) => dispatch({ type: "SET_PAGE", payload: page }),
setPeople: (people) => dispatch({ type: "SET_PEOPLE", payload: people }),
fetchPeople: () => fetchPeople(dispatch),
currentPage: state.currentPage,
people: state.people
};
return (
<MyContext.Provider value={providerValue}>{children}</MyContext.Provider>
);
};
index.js
import React from "react";
import ReactDOM from "react-dom";
import { MyProvider } from "../src/MyProvider";
import { PeoplePageComponent } from "../src/PeoplePageComponent";
const App = () => {
return (
<MyProvider>
<main>
<PeoplePageComponent />
</main>
</MyProvider>
);
};
ReactDOM.render(<App />, document.getElementById("container"));
PeoplePageComponent.test.jsx
import { render } from "#testing-library/react";
import { PagePeopleComponent } from "../PeoplePageComponent";
import { MyContext } from "../MyProvider";
const mockedContextValue = {
setPage: jest.fn(),
setPeople: jest.fn(),
currentPage: "loading",
people: null
};
test("sets page correctly", () => {
const contextValue = {
...mockedContextValue
};
const { rerender, container } = render(
<MyContext.Provider value={contextValue}>
<PagePeopleComponent />
</MyContext.Provider>
);
expect(container).toHaveTextContent(/This is the loading page/i);
rerender(
<MyContext.Provider value={{ ...contextValue, nominees: [] }}>
<PagePeopleComponent />
</MyContext.Provider>
);
expect(container).toHaveTextContent(/This is the hasPeople page/i);
});
Without changing the application code, you could not mock the context value, but the fetchPeople function. I assume in the real code it is a network request, thus it should be mocked anyway. Moreover not mocking the context provider makes the test more robust because it resemble more the way the software is used. In fact, it does not simulate context updates and it tests how the page manipulate the context.
import { render, screen } from "#testing-library/react";
import { PeoplePageComponent } from "../PeoplePageComponent";
import { MyProvider } from "../MyProvider";
import * as api from "../fetchPeople";
beforeEach(() => {
jest.spyOn(api, "fetchPeople").mockImplementation(async (dispatch) => {
setTimeout(() => {
dispatch({
type: "SET_PEOPLE",
payload: [
{ name: "Tommy", age: 24 },
{ name: "John", age: 25 },
],
});
}, 1);
});
});
test("sets page correctly", async () => {
render(
<MyProvider>
<PeoplePageComponent />
</MyProvider>
);
const loadingText = screen.getByText(/This is the loading page/i);
expect(loadingText).toBeInTheDocument();
expect(api.fetchPeople).toHaveBeenCalledTimes(1);
const peopleText = await screen.findByText(/This is the hasPeople page/i);
expect(peopleText).toBeInTheDocument();
});
I have a problem. As I understood hook useEffect doen't run.
I have action that should take data from server.
export const getProducts = () => {
return dispatch => {
dispatch(getProductsStarted());
fetch('https://shopserver.firebaseapp.com/get-products')
.then(res => {
dispatch(getProductsSuccess(res.json()));
})
.catch(err => {
dispatch(getProductsFailure(err.message));
});
}
}
const getProductsSuccess = todo => ({
type: "ADD_TODO_SUCCESS",
payload: {
...todo
}
});
const getProductsStarted = () => ({
type: "ADD_TODO_STARTED"
});
const getProductsFailure = error => ({
type: "ADD_TODO_FAILURE",
payload: {
error
}
});
I have a reducer.
const initialState = {
loading: false,
products: [],
error: null
}
export const ProductReducer = (state = initialState, action) => {
switch (action.type) {
case "ADD_TODO_SUCCESS":
return {
...state,
loading: false,
error: null,
todos: [...state.products, action.payload.products]
}
case "ADD_TODO_STARTED":
return {
...state,
loading: true
}
case "ADD_TODO_FAILURE":
return {
...state,
loading: false,
error: action.payload.error
}
default:
return state
}
}
And I have a Component where I want to render a result.
import React from 'react';
import { CardItem } from "./cardItem";
import { useSelector } from 'react-redux';
import { useEffect } from 'react';
import { getProducts } from '../Redux/Actions/productAction'
export const ProductCard = () => {
useEffect(() => {
getProducts();
console.log('111111')
})
const data = useSelector(state => state.ProductReducer.products);
return (
<div>
{data.map( element =>
CardItem (element)
)}
</div>
)
}
After rendering page nothing happens. ReduxDevTools shows that there was no send actions. Please, help me to fix it. Thank you.
I think you should be calling your async action like this :
import { useDispatch, useSelector } from 'react-redux';
[...]
export const ProductCard = () => {
const dispatch = useDispatch();
useEffect(() => {
// I guess getProducts is an async redux action using redux-thunk
dispatch(getProducts());
console.log('111111')
}, []);
[...]
}
I assume you want to load products only when component is born, so I pass an empty array as second argument for useEffect (https://reactjs.org/docs/hooks-reference.html#useeffect).
I'm trying to create an alert component, however for this I need to add an item (from anywhere) to the list of alerts in the state.
I have this code:
alertReducer.js:
import { SET_ALERT, GET_ALERTS, SET_ALERT_SHOWED } from "../actions/types";
const initialState = {
alerts: [
{
id: 0,
title: "teste",
message: "teste",
isShowed: false,
type: "success"
}
]
};
export default function(state = initialState, action) {
switch (action.type) {
case SET_ALERT:
return { ...state, alert: action.payload };
case SET_ALERT_SHOWED:
return {
...state,
alert: state.alerts.map(a =>
a.id === a.payload.id ? (a = action.payload) : a
)
};
case GET_ALERTS:
return { ...state };
default:
return state;
}
}
alertActions.js
import { SET_ALERT, GET_ALERTS, SET_ALERT_SHOWED } from "./types";
import axios from "axios";
export const getAlerts = () => dispatch => {
dispatch({
type: GET_ALERTS,
payload: null
});
};
export const setAlertShowed = alert => dispatch => {
dispatch({
type: SET_ALERT_SHOWED,
payload: null
});
};
export const setAlert = alert => dispatch => {
console.log("set alert:");
this.setState(state => {
state.alert.alerts.push(alert);
return null;
});
dispatch({
type: SET_ALERT,
payload: null
});
};
alerts.js (component)
import React from "react";
import { Link } from "react-router-dom";
import { Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
import {
Panel,
PanelHeader,
PanelBody
} from "./../../components/panel/panel.jsx";
import SweetAlert from "react-bootstrap-sweetalert";
import ReactNotification from "react-notifications-component";
import "react-notifications-component/dist/theme.css";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { getAlerts, setAlertShowed } from "../../actions/alertActions";
class Alerts extends React.Component {
constructor(props) {
super(props);
this.addNotification = this.addNotification.bind(this);
this.notificationDOMRef = React.createRef();
}
componentWillReceiveProps(nextProps) {
console.log("atualizou alertas");
console.log(this.props);
console.log(nextProps);
}
componentDidMount() {
this.props.getAlerts();
this.showAlerts();
}
showAlerts() {
const { alerts } = this.props;
alerts
.filter(a => !a.isShowed)
.map((a, i) => {
this.addNotification.call(this, a);
a.isShowed = true;
setAlertShowed(a);
});
}
addNotification(alert) {
this.notificationDOMRef.current.addNotification({
title: alert.title,
message: alert.message,
type: alert.type,
insert: "top",
container: "top-right",
animationIn: ["animated", "fadeIn"],
animationOut: ["animated", "fadeOut"],
dismiss: { duration: 2000 },
dismissable: { click: true }
});
}
render() {
const { alerts } = this.props;
return <ReactNotification ref={this.notificationDOMRef} />;
}
}
Alerts.propTypes = {
alerts: PropTypes.array.isRequired,
getAlerts: PropTypes.func.isRequired,
setAlertShowed: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
alerts: state.alert.alerts
});
export default connect(
mapStateToProps,
{ getAlerts, setAlertShowed }
)(Alerts);
So I have this helper I'm trying to do, it would serve so that from anywhere in the application I can trigger addAlert and generate a new alert, but I have no idea how to call the setAlert function inside the alertActions.js, what I was able to do is call the SET_ALERT through the store.dispatch, but apparently this is not triggering the setAlert or I am doing something wrong
import uuid from "uuid";
import { createStore } from "redux";
import { setAlert } from "../actions/alertActions";
import { SET_ALERT } from "../actions/types";
import alertReducer from "../reducers/alertReducer";
export function addAlert(state, title, message, type = "success") {
// const store = createStore(alertReducer);
// const state = store.getState();
const newalert = {
id: uuid.v4(),
title,
message,
isShowed: false,
type: type
};
console.log("state");
console.log(state);
// this.setState(state => {
// state.alert.alerts.push(alert);
// return null;
// });
// store.dispatch({
// type: SET_ALERT,
// payload: newalert
// });
// store.dispatch(setAlert(newalert));
// store.dispatch(SET_ALERT);
// this.setState(prevState => ({
// alert.alerts: [...prevState.alert.alerts, newalert]
// }))
}
PS. My react knowledge is very low yet and English it's not my primary language, if I don't make myself clear please ask anything.
Thank you.
Do like this:
// Create alert which you want to show
const alerts = [
{
id: 0,
title: "teste",
message: "teste",
isShowed: false,
type: "success"
}];
componentDidMount() {
this.props.getAlerts();
this.showAlerts();
// this will call alerts action
this.props.callAlert(alerts );
}
const mapDispatchToProps = dispatch=> ({
callAlert: (alert) => dispatch(setAlert(alert)),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
{ getAlerts, setAlertShowed }
)(Alerts);
Finally! I created the store by adding compose and applyMiddleware, I still have to study how this works best but it worked.
The helper code to add alert looks like this:
import uuid from "uuid";
import { createStore, dispatch, compose, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { setAlert } from "../actions/alertActions";
import alertReducer from "../reducers/alertReducer";
export function addAlert(title, message, type = "success") {
const store = createStore(alertReducer, compose(applyMiddleware(thunk)));
const newalert = {
id: uuid.v4(),
title,
message,
isShowed: false,
type: type
};
store.dispatch(setAlert(newalert));
}
I'm having some troubles testing that a prop is fired on my HOC.
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetchCurrentUser } from '../../actions/users';
import { getUser } from '../../reducers/users';
import User from '../../models/User';
export default Component => compose(
connect(state => ({
user: getUser(state),
}),
{ fetchCurrentUser }),
lifecycle({
componentDidMount() {
if (this.props.user instanceof User) return;
this.props.fetchCurrentUser();
},
}),
)(Component);
What I'd like to know is whether or not fetchCurrentUser is trigger when user is NOT a User instance.
So far I have that in my test:
it.only('fetches user if user is not a User instance', () => {
const setup = () => {
const props = {
user: 'string',
fetchCurrentUser: jest.fn(),
};
const enzymeWrapper = mounting(props);
return {
props,
enzymeWrapper,
};
};
// That returns 0 so false
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
It seems like I can't replace the props doing it this way. If I log this.props in the lifecycle method, I never see user: 'string'
Thanks in advance
You would need to shallow mount the component in order to test its functionality.
it.only('fetches user if user is not a User instance', () => {
const setup = () => {
const props = {
user: 'string',
fetchCurrentUser: jest.fn(),
};
// shallow render the component
const enzymeWrapper = shallow(<Component {...props} />)
return {
props,
enzymeWrapper,
};
};
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
OK so, with shubham-khatri's help, here's what I did to make it work.
Separated the component into 2 different ones, and tested the one with the call only. That way I could mock the passed props from the test.
Component:
import { connect } from 'react-redux';
import { compose, lifecycle } from 'recompose';
import { fetchCurrentUser } from '../../actions/users';
import { getUser } from '../../reducers/users';
import User from '../../models/User';
const Connected = connect(state => ({
user: getUser(state),
}),
{ fetchCurrentUser });
export const Enhanced = lifecycle({
componentDidMount() {
if (this.props.user instanceof User) return;
this.props.fetchCurrentUser();
},
});
export default Component => compose(
Connected,
Enhanced,
)(Component);
Test:
describe('Fetching user', () => {
const setup = (moreProps) => {
const props = {
fetchCurrentUser: jest.fn(),
...moreProps,
};
const EnhancedStub = compose(
Enhanced,
)(Component);
const enzymeWrapper = shallow(
<EnhancedStub {...props} />,
);
return {
props,
enzymeWrapper,
};
};
it('fetches user if user is not a User instance', () => {
expect(setup().props.fetchCurrentUser.mock.calls.length).toEqual(1);
});
it('does NOT fetch user if user is a User instance', () => {
expect(setup({ user: new User({ first_name: 'Walter' }) }).props.fetchCurrentUser.mock.calls.length).toEqual(0);
});
});
Hope that helps someone.