Jest-React hook Testing : How to call a setState inside a custom function using useEffect - reactjs

I am trying to set mockPatient data and wanted to test if the 'sortByCaseFn ' function is called by the useEffect.
Here is my sourcecode:
Patient.tsx
const [patients, setPatients] = useState([]);
const [sortBy, setSortBy] = useState('events');
const [fetched, setFetched] = useState(false);
const dispatch = useAppDispatch();
const props = useAppSelector((state) => state.myPatientProps);
const getPatientData = (): void => {
dispatch(MyPatientActions.getMyPatientsData());
};
const sortByCaseFn = (sortBy, list) => {
let patientsToSort = [...list];
if (sortBy.includes('events'))
patientsToSort.sort(
sorter.byPropertiesOf(['-ActiveEventsCount', 'LastName'])
);
if (sortBy.includes('vae'))
patientsToSort.sort(sorter.byPropertiesOf(['-VaeStatus']));
console.log('patientsToSort---', patientsToSort);
setPatients(patientsToSort);
};
useEffect(() => {
if (!fetched) {
getPatientData();
}
}, []);
useEffect(() => {
console.log('setpatients called .. ', patients);
}, [patients]);
useEffect(() => {
const saved_sortby = localStorage.getItem('sortby');
if (saved_sortby) {
sortByCaseFn(saved_sortby, props.myPatientDetails);
} else sortByCaseFn('events', props.myPatientDetails);
setFetched(true);
}, [props.myPatientDetails]);
useEffect(() => {
sortByCaseFn(sortBy, patients);
}, [sortBy]);
return (
<> Render Patient List </> )
My Test Code :
Patients.test.tsx
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: jest.fn()
}));
export const setHookTestState = (newState: any) => {
const setStateMockFn = () => {};
return Object.keys(newState).reduce((acc, val) => {
acc = acc?.mockImplementationOnce(() => [newState[val], setStateMockFn]);
return acc;
}, jest.fn());
};
describe('My Patient Screen', () => {
const useSelectorMock = reactRedux.useSelector as jest.Mock<any>;
const useDispatchMock = reactRedux.useDispatch as jest.Mock<any>;
beforeEach(() => {
useSelectorMock.mockImplementation((selector) => selector(mockStore));
useDispatchMock.mockImplementation(() => () => {});
});
afterEach(() => {
useDispatchMock.mockClear();
useSelectorMock.mockClear();
});
const mockInitialState = {
myPatientDetails: vaeMock,
fetching: false,
failedMsg: '',
requestPayload: {}
};
const mockStore = {
counter: undefined,
menu: undefined,
selectPatientProps: undefined,
myPatientProps: mockInitialState
};
test('validate sorting by events', async (done) => {
React.useState = setHookTestState({
patients: vaeMock,
sortBy: 'vae',
fetched: 'false'
});
const {
getByText,
getByRole,
getByTestId,
getAllByTestId,
findAllByTestId,
queryByText,
container
} = render(<Mypatient />);
await waitFor(() => {
expect(getByText('Ander, Sam')).toBeDefined();
});
const list = getAllByTestId('patientname');
expect(within(list[0]).getByText('Sara, Jone')).toBeInTheDocument(); //Fails here as Sorting doesnt happen
console.log('....list ', list);
});
});
My Observations:
The 'vaeMock' data that I set in redux state 'mockInitialState' is successfully sent as props
The 'vaeMock' data that I set in component state using setHookTestState is also set successfully.
The lifecycle events happens like this -
a. setPatients() is called using the component state data.
b. using props that is sent , sortByCaseFn is called but setPatients is not called.
c. again using the component state , sortByCaseFn is called but setPatients is not set.
Without setting the component state variables runs into a TypeError: Undefined is not iterable.
All Iam trying to do is - send a mockData to a component that uses useDispatch, useEffects
and sort the data on the component mount and initialize to local state variable.

Related

useState updates twice

I have a component that gets a value from the local storage and does a useQuery to get some data:
const DashboardComponent = () => {
const [filterState, setFilter] = useState(false);
const returnFilteredState = async () => {
return await localforage.getItem<boolean>('watchedAndReviewedFilterd') || false;
};
useEffect(() => {
returnFilteredState().then((value) => {
setFilter(value);
});
}, []);
const {error, loading, data: {moviesFromUser: movies} = {}} =
useQuery(resolvers.queries.ReturnMoviesFromUser, {
variables: {
userId: currentUserVar().id,
filter: filterState,
},
});
The problem is that the ReturnMoviesFromUser query is called twice. I think it's because of the filterState variable. If I set the filter: true the ReturnMoviesFromUser is only called once.

Jest - destructure property

export const AppLayout: React.FunctionComponent = React.memo(({ children }) => {
// Application main layout component name
AppLayout.displayName = getComponentName('App-Layout');
const { isAuthenticated } = useAuth();
const { sendRequest } = useApiService();
React.useEffect(() => {
const fetchData = async () => {
try {
...
} catch (err) {
console.error(err);
}
};
isAuthenticated() && fetchData();
}, []);
describe('App General component', () => {
const useAuth = jest.fn();
const useApiService = jest.fn();
const isAuthenticated = true;
const props = {};
const renderComponent = () => render(
<AppLayout/>
);
it('should render without errors', () => {
renderComponent();
});
/**
* Validate current user exist in session
* #returns {boolean}
*/
const isAuthenticated = React.useCallback((): boolean => {
return Boolean(user);
}, [user]);
How can I set isAuthenticated to true so I can avoid the error
TypeError: Cannot destructure property 'isAuthenticated' of
const mockUseAuthIsAuthenticated = jest.fn(() => false);
const mockUseAuth = jest.fn(() => ({
isAuthenticated: mockUseAuthIsAuthenticated,
});
jest.mock("../hooks/useAuth", mockUseAuth);
describe('My test case', () => {
it(`should return authenticated=TRUE`, () => {
// Given
mockUseAuthIsAuthenticated.mockImplementationOnce(
() => true
);
// When
// assuming `render` comes from the react testing-library
render(<ComponentThatCallsTheHook />);
// Then
expect(mockUseAuthIsAuthenticated).toHaveBeenCalledOnce();
// ... more expectations
});
});
You should mock the useAuth hook like this:
jest.mock("yourUseAuthPath", () => ({
useAuth: () => ({
isAuthenticated: () => true
}),
}));
describe('App General component', () => {
...
}
n.b. You should replace the yourUseAuthPath with the correct path where you get the useAuth from. Example:
import { useAuth } from "yourUseAuthPath";
Some official docs here: https://jestjs.io/docs/mock-functions#mocking-partials

react state not updating inside callabck

I'm not understanding why the following code, the callback onSocketMessage is not using the new acquisition state. inside the useEffect the state is correctly updated, but the function is not evaluated again...i've also tryed using useCallback with acquisition as dependency but nothing changed.
const Ac = () => {
const [acquisition, setAcquisition] = useState({ data: {} })
const [loading, setLoading] = useState(true)
const socket = useRef(null);
const onSocketMessage = (message) => {
console.log(acquisition) // this is always initial state
let { data } = acquisition
data.input[message.index] = message.input
setAcquisition(prevState => ({ ...prevState, data }));
}
useEffect(() => {
fetchCurrentAcquisition(acquisition => {
setAcquisition(acquisition)
setLoading(false)
socket.current = newSocket('/acquisition', () => console.log('connected'), onSocketMessage);
})
return () => socket.current.disconnect()
}, [])
console.log(acquisition)
You are logging a stale closure you should try the following instead:
const onSocketMessage = useCallback((message) => {
setAcquisition((acquisition) => {
//use acquisition in the callback
console.log(acquisition);
//you were mutating state here before
return {
...acquisition,
data: {
...acquisition.data,
input: {
//not sure if this is an array or not
//assimung it is an object
...acquisition.data.input,
[message.index]: message.input,
},
},
};
});
}, []); //only created on mount
useEffect(() => {
fetchCurrentAcquisition((acquisition) => {
setAcquisition(acquisition);
setLoading(false);
socket.current = newSocket(
'/acquisition',
() => console.log('connected'),
onSocketMessage
);
});
return () => socket.current.disconnect();
//onSocketMessage is a dependency of the effect
}, [onSocketMessage]);

Can I ignore exhaustive-deps warning for useContext?

In my react-typescript application, I am trying to use a context provider that encapsulates properties and methods and exposes them for a consumer:
const StockPriceConsumer: React.FC = () => {
const stockPrice = useContext(myContext);
let val = stockPrice.val;
useEffect(() => {
stockPrice.fetch();
}, [val]);
return <h1>{val}</h1>;
};
The problem is the following warning:
React Hook useEffect has a missing dependency: 'stockPrice'. Either
include it or remove the dependency
array. eslint(react-hooks/exhaustive-deps)
To me it does not make any sense to include the stockPrice (which is basically the provider's API) to the dependencies of useEffect. It only makes sense to include actual value of stock price to prevent infinite calls of useEffect's functions.
Question: Is there anything wrong with the approach I am trying to use or can I just ignore this warning?
The provider:
interface StockPrice {
val: number;
fetch: () => void;
}
const initialStockPrice = {val: NaN, fetch: () => {}};
type Action = {
type: string;
payload: any;
};
const stockPriceReducer = (state: StockPrice, action: Action): StockPrice => {
if (action.type === 'fetch') {
return {...state, val: action.payload};
}
return {...state};
};
const myContext = React.createContext<StockPrice>(initialStockPrice);
const StockPriceProvider: React.FC = ({children}) => {
const [state, dispatch] = React.useReducer(stockPriceReducer, initialStockPrice);
const contextVal = {
...state,
fetch: (): void => {
setTimeout(() => {
dispatch({type: 'fetch', payload: 200});
}, 200);
},
};
return <myContext.Provider value={contextVal}>{children}</myContext.Provider>;
};
I would recommend to control the whole fetching logic from the provider:
const StockPriceProvider = ({children}) => {
const [price, setPrice] = React.useState(NaN);
useEffect(() => {
const fetchPrice = () => {
window.fetch('http...')
.then(response => response.json())
.then(data => setPrice(data.price))
}
const intervalId = setInterval(fetchPrice, 200)
return () => clearInterval(intervalId)
}, [])
return <myContext.Provider value={price}>{children}</myContext.Provider>;
};
const StockPriceConsumer = () => {
const stockPrice = useContext(myContext);
return <h1>{stockPrice}</h1>;
};
...as a solution to a couple of problems from the original appproach:
do you really want to fetch only so long as val is different? if the stock price will be the same between 2 renders, the useEffect won't execute.
do you need to create a new fetch method every time <StockPriceProvider> is rendered? That is not suitable for dependencies of useEffect indeed.
if both are OK, feel free to disable the eslint warning
if you want to keep fetching in 200ms intervals so long as the consumer is mounted:
// StockPriceProvider
...
fetch: useCallback(() => dispatch({type: 'fetch', payload: 200}), [])
...
// StockPriceConsumer
...
useEffect(() => {
const i = setInterval(fetch, 200)
return () => clearInterval(i)
}, [fetch])
...
The important concept here is that react compares the objects by reference equality. Meaning that every time the reference (and not the content) changes it will trigger a re-render. As a rule of thumb, you always need to define objects/functions that you want to pass to child components by useCallback and useMemo.
So in your case:
The fetch function will become:
const fetch = useCallback(() => {
setTimeout(() => {
dispatch({ type: 'fetch', payload: 200 });
}, 1000);
}, []);
The empty array means that this function will be only defined when the component is mounted. And then:
let {val, fetch} = stockPrice;
useEffect(() => {
fetch();
}, [val, fetch]);
This means the useEffect's callback will execute only when fetch or val changes. Since fetch will be defined only once, in practice it means only val changes are gonna trigger the effect's callback.
Also, I can imagine you want to trigger the fetch only when isNaN(val) so:
let {val, fetch} = stockPrice;
useEffect(() => {
if(isNaN(val)) {
fetch();
}
}, [val, fetch]);
All that being said, there's a bigger issue with this code!
You should reconsider the way you use setTimeout since the callback can run when the component is already unmounted and that can lead to a different bug. In these cases you should useEffect and clear any async operation before unmounting the component. So here's my suggestion:
import React, { useCallback, useContext, useEffect } from 'react';
interface StockPrice {
val: number;
setFetched: () => void;
}
const initialStockPrice = { val: NaN, setFetched: () => { } };
type Action = {
type: string;
payload: any;
};
const stockPriceReducer = (state: StockPrice, action: Action): StockPrice => {
if (action.type === 'fetch') {
return { ...state, val: action.payload };
}
return { ...state };
};
const myContext = React.createContext<StockPrice>(initialStockPrice);
const StockPriceProvider: React.FC = ({ children }) => {
const [state, dispatch] = React.useReducer(
stockPriceReducer,
initialStockPrice
);
const setFetched = useCallback(() => {
dispatch({ type: 'fetch', payload: 200 });
}, []);
const contextVal = {
...state,
setFetched,
};
return <myContext.Provider value={contextVal}>{children}</myContext.Provider>;
};
const StockPriceConsumer: React.FC = () => {
const stockPrice = useContext(myContext);
const {val, setFetched} = stockPrice;
useEffect(() => {
let handle = -1;
if(isNaN(val)) {
let handle = setTimeout(() => { // Or whatever async operation
setFetched();
}, 200);
}
return () => clearTimeout(handle); // Clear timeout before unmounting.
}, [val, setFetched]);
return <h1>{stockPrice.val.toString()}</h1>;
};

Test react forceUpdate custom hook useEffect/useState

I created a custom hook to force a component to update but I'm having issues figuring out how to write a unit test with jest.
This is the hook
function useForceUpdate(condition) {
const [, setState] = useState(0);
const forceUpdate = () => setState(1);
useEffect(() => {
if (condition) {
forceUpdate();
}
}, [condition]);
}
export default useForceUpdate;
I was able to successfully test this hook this way
import React from "react";
import useForceUpdate from "hooks/use-force-update";
const Component = ({ shouldUpdate }) => {
const hasUpdated = useForceUpdate(shouldUpdate);
return <div>{hasUpdated}</div>;
};
describe("useForceUpdate", () => {
let subject;
let props;
beforeEach(() => {
props = { shouldUpdate: true };
subject = memoize(() => mount(<Component {...props} />));
});
describe("when the condition is true", () => {
it("it calls forceUpdate", () => {
expect(
subject()
.find("div")
.text()
).toBe("1");
});
});
describe("when the condition is false", () => {
beforeEach(() => {
props = { shouldUpdate: false };
});
it("it does not call forceUpdate", () => {
expect(
subject()
.find("div")
.text()
).toBe("0");
});
});
});

Resources