I'm very annoyed since the morning cause I can't test my custom hooks without this infinite loop.
My custom hook look like :
useGetObj1.tsx
export function useGetObj1(): [
{ obj1: IObj1 | null; obj2: IObj2 | null } | null,
Function
] {
const User = useSelector((state: IStoreState) => state.User);
const history: History = useHistory();
const resState = useState<{ obj1: IObj1 | null; obj2: IObj2 | null } | null>(null);
const setState = resState[1];
useEffect(() => {
async function setStateValue() {
let tmp = null;
const res = await ParseVerifObj1(User, history.location.pathname);
if (typeof res !== "number") {
res
? (tmp = {
obj1: res.obj1,
obj2: res.obj2,
})
: (tmp = { obj1: null, obj2: null });
setState(tmp);
} else if (res === -1) history.push("/HomePage");
}
setStateValue();
}, [history.location.pathname, User, setState]);
return resState;
}
My test looks like :
useGetObj1.spec.js
jest.mock("axios");
jest.mock("react-router-dom", () => ({
useHistory: () => ({
push: jest.fn(),
location: {
pathname: "/path/web",
},
}),
}));
// make setup-jest file and put it later
import "axios";
import "babel-polyfill";
//
import React from "react";
import { mount } from "enzyme";
import { Provider } from "react-redux";
// import ParseVerifObj1 to mock it
import * as ParseVerifObj1 from "../path";
// custom hook tested here
import { useGetObj1 } from "../path";
import configureStore from "redux-mock-store";
// act to handle the Promise and the re-render of the renderHook
import { act } from "react-dom/test-utils";
// Utils Initial store
import { initialStore } from "../path";
import routeData from "react-router";
const mockStore = configureStore();
const mockLocation = {
pathname: "/",
hash: "",
search: "",
state: "",
};
jest.spyOn(routeData, "useLocation").mockReturnValue(mockLocation);
describe("useGetObj1.spec.js", () => {
let store;
beforeEach(() => {
store = mockStore(initialStore);
jest.clearAllMocks();
});
let results;
let wrapper;
const renderHook = (hook) => {
function HookWrapper() {
results = hook();
return null;
}
wrapper = mount(
<Provider store={store}>
<HookWrapper />
</Provider>
);
return results;
};
describe("Testing the custom hook : useGetObj1", () => {
it("useGetLesson is ran", async () => {
await act(async () => renderHook(useGetObj1));
expect(wrapper).toBeTruthy();
});
it("Testing the output adress values get from the ParseVerifObj1 function", async () => {
const spy = jest.spyOn(ParseVerifObj1, "ParseVerifObj1");
const lesson = Symbol("Obj1");
const session = Symbol("Obj2");
spy.mockReturnValue({
Obj1,
Obj2,
});
await act(async () => renderHook(useGetObj1));
expect(spy).toHaveBeenCalledWith(
initialStore.User,
"/path/web"
);
expect(spy).toHaveBeenCalled();
expect(results[0].Obj1).toBe(Obj1);
expect(results[0].Obj2).toBe(Obj2);
expect(results[1]).toBeInstanceOf(Function);
});
I know it's not a good thing to put history in the useEffect's dependencies but I got a warning like
Line 39:6: React Hook useEffect has a missing dependency: 'history'. Either includes it or remove the dependency array
and I tried a lot of different implementation but no one fixed the problem and compile without a warning
thx for helping :)
_Solution
The solution for me was to use useRef to avoid the dependencies requirement like this :
const history: History = useRef(useHistory());
with the ref you do not need anymore to put it as a dependency
Related
I have this module that I want to mock:
import { createContext, useContext } from 'react'
const SomeContext = createContext({
set: () => null,
reset: () => null,
})
export const useSomeContext = () => useContext(SomeContext)
... and here is how it is used in a custom react hook:
import { useSomeContext } from './someContext'
export const useCustomHook = () => {
const { reset, set } = useSomeContext()
useEffect(() => {
set()
return reset
}, [reset, set])
...
}
I am trying to test that the function set is called when the useCustomHook component renders. Here is my attempt at writing such a test:
import * as useSomeContextModule from '../someContext'
import { useCustomHook } from '../useCustomHook'
const mockReset = jest.fn()
const mockSet = jest.fn()
jest.mock('../someContext', () => {
const originalModule = jest.requireActual('../someContext')
return {
__esModule: true,
...originalModule,
useSomeContext: jest.fn(() => ({
reset: mockReset,
set: mockSet,
})),
}
})
test('set method is called when useCustomHook renders', () => {
useCustomHook()
expect(mockSet).toHaveBeenCalled()
})
But, i get this error:
TypeError: Cannot destructure property 'reset' of '(0 , _index.useSomeContext)(...)' as it is undefined.
7 |
8 | export const useCustomHook = () => {
> 9 | const { reset, set } = useSomeContext()
Instead of using jest.mock I used jest.spyOn:
import * as useSomeContextModule from '../someContext'
import { useCustomHook } from '../useCustomHook'
import { renderHook, cleanup } from '#testing-library/react-hooks'
const mockReset = jest.fn()
const mockSet = jest.fn()
beforeEach(() => {
jest.spyOn(useSomeContextModule, 'useSomeContext').mockReturnValue({
reset: mockReset,
set: mockSet,
})
})
test('set method is called when useCustomHook renders', () => {
renderHook(() => useCustomHook())
expect(mockSet).toHaveBeenCalledOnce()
})
test('reset method is called when useCustomHook unmounts', async () => {
renderHook(() => useCustomHook())
await cleanup()
expect(mockReset).toHaveBeenCalledOnce()
})
I need to mock my custom hook when unit testing React component. I've read some stackoverflow answers but haven't succeeded in implementing it correctly.
I can't use useAuth without mocking it as it depends on server request and I'm only writing unit tests at the moment.
//useAuth.js - custom hook
const authContext = createContext();
function useProvideAuth() {
const [accessToken, setAccessToken] = useState('');
const [isAuthenticated, setAuthenticated] = useState(
accessToken ? true : false
);
useEffect(() => {
refreshToken();
}, []);
const login = async (loginCredentials) => {
const accessToken = await sendLoginRequest(loginCredentials);
if (accessToken) {
setAccessToken(accessToken);
setAuthenticated(true);
}
};
const logout = async () => {
setAccessToken(null);
setAuthenticated(false);
await sendLogoutRequest();
};
const refreshToken = async () => {
const accessToken = await sendRefreshRequest();
if (accessToken) {
setAccessToken(accessToken);
setAuthenticated(true);
} else setAuthenticated(false);
setTimeout(async () => {
refreshToken();
}, 15 * 60000 - 1000);
};
return {
isAuthenticated,
accessToken,
login,
logout
};
}
export function AuthProvider({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
AuthProvider.propTypes = {
children: PropTypes.any
};
const useAuth = () => {
return useContext(authContext);
};
export default useAuth;
The test I've written
//mainPage.test.js
import React from 'react';
import { render, screen } from '#testing-library/react';
import Main from '../main/mainPage';
describe('Main when !isAuthenticated', () => {
beforeEach(() => {
jest.mock('../auth/useAuth', () => {
const originalModule = jest.requireActual('../auth/useAuth');
return {
__esModule: true,
...originalModule,
default: () => ({
isAuthenticated: false,
login: jest.fn,
logout: jest.fn
})
};
});
});
afterEach(() => {
jest.resetModules();
});
it('displays image and login form', () => {
render(<Main />);
const image = screen.getByRole('img');
const form = document.querySelector('[data-testid=loginForm]');
expect(image).toBeInTheDocument();
expect(form).toBeInTheDocument();
});
});
However, I get this error.
TypeError: Cannot read properties of undefined (reading 'isAuthenticated')
7 |
8 | function Main() {
> 9 | const isAuthenticated = useAuth().isAuthenticated;
| ^
10 | const location = useLocation();
11 |
12 | if (isAuthenticated)
at Main (src/main/mainPage.js:9:26)
at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18)...
I've been also trying to use spyOn but nothing helped. What exactly do I need to change to make the mock work?
The mocking should happen before any describe block:
import React from 'react';
import { render, screen } from '#testing-library/react';
import Main from '../main/mainPage';
jest.mock('../auth/useAuth', () => {
const originalModule = jest.requireActual('../auth/useAuth');
return {
__esModule: true,
...originalModule,
default: () => ({
isAuthenticated: false,
login: jest.fn,
logout: jest.fn
})
};
});
describe('Main when !isAuthenticated', () => {
I am a new React developer, implementing global state in my app. Im using useReducer with Context API to cache form search data, but I feel I'm using the reducer wrong, even if it works. I am preparing payload BEFORE calling dispatchSearchData, instead of doing it directly inside reducer:
import React, { createContext, useReducer, useMemo, useEffect } from "react";
const initialData = {
from: "",
to: "",
date_go: "",
date_back: "",
passengers: "",
};
const dataReducer = (searchData, newData) => {
if (newData === null) {
localStorage.removeItem("currentSearchData");
return initialData;
}
return { ...searchData, ...newData };
};
const localData = JSON.parse(localStorage.getItem("currentSearchData"));
export const SearchDataContext = createContext({});
export const SearchDataProvider = (props) => {
const [searchData, dispatchSearchData] = useReducer(dataReducer, localData || initialData);
const searchDataValue = useMemo(
() => ({
searchData,
setSearchData,
}),
[searchData, setSearchData],
);
useEffect(() => {
localStorage.setItem("currentSearchData", JSON.stringify(searchData));
}, [searchData]);
return <SearchDataContext.Provider value={searchDataValue}>{props.children}</SearchDataContext.Provider>;
};
An example of calling it:
let search = (e) => {
e.preventDefault();
dispatchSearchData(formData);
setServiceData(null);
}
I'm quite new to React Hooks/Context so I'd appreciate some help. Please don' t jump on me with your sharp teeth. I Checked other solutions and some ways i've done this before but can't seem to get it here with the 'pick from the list' way.
SUMMARY
I need to get the municipios list of names inside of my const 'allMunicipios'(array of objects) inside of my Search.js and then display a card with some data from the chosen municipio.
TASK
Get the data from eltiempo-net REST API.
Use Combobox async element from Elastic UI to choose from list of municipios.
Display Card (from elastic UI too) with some info of chosen municipio.
It has to be done with function components / hooks. No classes.
I'd please appreciate any help.
WHAT I'VE DONE
I've created my reducer, context and types files in a context folder to fecth all data with those and then access data from the component.
I've created my Search.js file. Then imported Search.js in App.js.
I've accesed the REST API and now have it in my Search.js
PROBLEM
Somehow I'm not beeing able to iterate through the data i got.
Basically i need to push the municipios.NOMBRE from api to the array const allMunicipios in my search.js component. But when i console log it it gives me undefined. Can;t figure out why.
I'll share down here the relevant code/components. Thanks a lot for whoever takes the time.
municipiosReducer.js
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
export default (state, action) => {
switch (action.type) {
case SEARCH_MUNICIPIOS:
return {
...state,
municipios: action.payload,
};
case GET_MUNICIPIO:
return {
...state,
municipio: action.payload,
};
case CLEAR_MUNICIPIOS:
return {
...state,
municipios: [],
};
case GET_WEATHER: {
return {
...state,
weather: action.payload,
};
}
default:
return state;
}
};
municipiosContext.js
import { createContext } from "react";
const municipiosContext = createContext();
export default municipiosContext;
MunicipiosState.js
import React, { createContext, useReducer, Component } from "react";
import axios from "axios";
import MunicipiosContext from "./municipiosContext";
import MunicipiosReducer from "./municipiosReducer";
import {
SEARCH_MUNICIPIOS,
CLEAR_MUNICIPIOS,
GET_MUNICIPIO,
GET_WEATHER,
} from "./types";
const MunicipiosState = (props) => {
const initialState = {
municipios: [],
municipio: {},
};
const [state, dispatch] = useReducer(MunicipiosReducer, initialState);
//Search municipios
//In arrow functions 'async' goes before the parameter.
const searchMunicipios = async () => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios`
// 08 means barcelona province. This should give me the list of all its municipios
);
dispatch({
type: SEARCH_MUNICIPIOS,
payload: res.data.municipios,
});
};
//Get Municipio
const getMunicipio = async (municipio) => {
const res = await axios.get(
`https://www.el-tiempo.net/api/json/v2/provincias/08/municipios/${municipio.CODIGOINE}`
//CODIGOINE is in this REST API kind of the ID for each municipio.
//I intent to use this later to get the weather conditions from each municipio.
);
dispatch({ type: GET_MUNICIPIO, payload: res.municipio });
};
const dataMunicipiosArray = [searchMunicipios];
//Clear Municipios
const clearMunicipios = () => {
dispatch({ type: CLEAR_MUNICIPIOS });
};
return (
<MunicipiosContext.Provider
value={{
municipios: state.municipios,
municipio: state.municipio,
searchMunicipios,
getMunicipio,
clearMunicipios,
dataMunicipiosArray,
}}
>
{props.children}
</MunicipiosContext.Provider>
);
};
export default MunicipiosState;
Search.js
import "#elastic/eui/dist/eui_theme_light.css";
import "#babel/polyfill";
import MunicipiosContext from "../contexts/municipiosContext";
import MunicipiosState from "../contexts/MunicipiosState";
import { EuiComboBox, EuiText } from "#elastic/eui";
import React, { useState, useEffect, useCallback, useContext } from "react";
const Search = () => {
const municipiosContext = useContext(MunicipiosContext);
const { searchMunicipios, municipios } = MunicipiosState;
useEffect(() => {
return municipiosContext.searchMunicipios();
}, []);
const municipiosFromContext = municipiosContext.municipios;
const bringOneMunicipio = municipiosContext.municipios[0];
let municipiosNames = municipiosFromContext.map((municipio) => {
return { label: `${municipio.NOMBRE}` };
});
console.log(`municipiosFromContext`, municipiosFromContext);
console.log(`const bringOneMunicipio:`, bringOneMunicipio);
console.log(`municipiosNames:`, municipiosNames);
const allMunicipios = [
{ label: "santcugat" },
{ label: "BARCELONETA" },
{ label: "BARCE" },
];
const [selectedOptions, setSelected] = useState([]);
const [isLoading, setLoading] = useState(false);
const [options, setOptions] = useState([]);
let searchTimeout;
const onChange = (selectedOptions) => {
setSelected(selectedOptions);
};
// combo-box
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, []);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
return (
<div>
<EuiComboBox
placeholder="Search asynchronously"
async
options={options}
selectedOptions={selectedOptions}
isLoading={isLoading}
onChange={onChange}
onSearchChange={onSearchChange}
/>
<button>Lista de municipios</button>
</div>
);
};
export default Search;
also the
Home.js
import React, { useState } from "react";
import { EuiComboBox, EuiText } from "#elastic/eui";
// import { DisplayToggles } from "../form_controls/display_toggles";
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import Search from "./Search";
import MunicipioCard from "./MunicipioCard";
const Home = () => {
return (
<div>
<EuiText grow={false}>
<h1>Clima en la provincia de Barcelona</h1>
<h2>Por favor seleccione un municipio</h2>
</EuiText>
<Search />
<MunicipioCard />
</div>
);
};
export default Home;
App.js
import "#babel/polyfill";
import "#elastic/eui/dist/eui_theme_light.css";
import { EuiText } from "#elastic/eui";
import React from "react";
import Home from "./components/Home";
import MunicipiosState from "./contexts/MunicipiosState";
import "./App.css";
function App() {
return (
<MunicipiosState>
<div className="App">
<EuiText>
<h1>App Component h1</h1>
</EuiText>
<Home />
</div>
</MunicipiosState>
);
}
export default App;
You are using forEach and assigning the returned value to a variable, however forEach doesn't return anything. You should instead use map
let municipiosNames = municipiosFromContext.map((municipio) => {
return `label: ${municipio.NOMBRE}`;
});
As per your comment:
you data is loaded asynchronously, so it won't be available on first render and since functional components depend on closures, you onSearchChange function takes the value from the closure at the time of creation and even if you have a setTimeout within it the updated value won't reflect
The solution here is to add municipiosFromContext as a dependency to useEffect
const onSearchChange = useCallback((searchValue) => {
setLoading(true);
setOptions([]);
clearTimeout(searchTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
searchTimeout = setTimeout(() => {
// Simulate a remotely-executed search.
setLoading(false);
setOptions(
municipiosNames.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase())
)
);
}, 1200);
}, [municipiosFromContext]);
useEffect(() => {
// Simulate initial load.
onSearchChange("");
}, [onSearchChange]);
I am trying to develop a custom hook which seems to be pretty easy but I am getting an error
Uncaught Invariant Violation: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
This is my hook:
import React, { useState, useEffect } from 'react';
const useInfiniteScroll = (isLastPage: boolean, fetchFn: any) => {
const [pageCount, setPageCount] = useState(0);
const triggerFetchEvents = (): void => {
let response;
setPageCount(() => {
if (!isLastPage) {
response = fetchFn(pageCount + 1, 5, 'latest');
}
return pageCount + 1;
});
return response;
};
useEffect(() => {
triggerFetchEvents();
}, []);
return pageCount;
};
export default useInfiniteScroll;
And the component here I am calling it:
import React, { FC } from 'react';
import { connect } from 'react-redux';
import { fetchEvents } from '../../shared/actions/eventActions';
import { AppState } from '../../shared/types/genericTypes';
import EventModel from '../../shared/models/Event.model';
import EventListPage from '../../components/events/EventListPage';
import useInfiniteScroll from '../../shared/services/triggerInfiniteScroll';
type Props = {
fetchEvents?: any;
isLastPage: boolean;
eventsList?: EventModel[];
};
const mapState: any = (state: AppState, props: Props): Props => ({
eventsList: state.eventReducers.eventsList,
isLastPage: state.eventReducers.isLastPage,
...props
});
const actionCreators = {
fetchEvents
};
export const EventsScene: FC<Props> = props => {
const { eventsList, fetchEvents, isLastPage } = props;
const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);
useIn();
// const [pageCount, setPageCount] = useState(0);
// const triggerFetchEvents = (): void => {
// let response;
// setPageCount(() => {
// if (!isLastPage) {
// response = fetchEvents(pageCount + 1, 1, 'latest');
// }
// return pageCount + 1;
// });
// return response;
// };
// useEffect(() => {
// triggerFetchEvents();
// }, []);
if (!eventsList || !eventsList.length) return null;
return (
<EventListPage
eventsList={eventsList}
isLastPage={isLastPage}
triggerFetchEvents={useIn}
/>
);
};
export default connect(
mapState,
actionCreators
)(EventsScene);
I left the commented code there to show you that if I uncomment the code and remove useInfiniteScroll then it works properly.
What could I be missing?
UPDATE:
This is EventListPage component
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import EventModel from '../../shared/models/Event.model';
import { formatDate } from '../../shared/services/date';
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Card from 'react-bootstrap/Card';
type Props = {
eventsList?: EventModel[];
isLastPage: boolean;
triggerFetchEvents: any;
};
export const EventListPage: React.FC<Props> = props => {
const { eventsList, triggerFetchEvents, isLastPage } = props;
const [isFetching, setIsFetching] = useState(false);
const fetchMoreEvents = (): Promise<void> =>
triggerFetchEvents().then(() => {
setIsFetching(false);
});
const handleScroll = (): void => {
if (
document.documentElement.offsetHeight -
(window.innerHeight + document.documentElement.scrollTop) >
1 ||
isFetching
) {
return;
}
return setIsFetching(true);
};
useEffect(() => {
if (isFetching) return;
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
useEffect(() => {
if (!isFetching) return;
if (!isLastPage) fetchMoreEvents();
}, [isFetching]);
if (!eventsList) return null;
return (
<Container className='article-list mt-5'>
///...
</Container>
);
};
export default EventListPage;
In EventsScene, change useInfiniteScroll to be invoked directly at the function body top-level (not sure why you are creating this indirection in the first place):
// before
const useIn = () => useInfiniteScroll(isLastPage, fetchEvents);
useIn();
// after
useInfiniteScroll(isLastPage, fetchEvents)
React expects Hook calls to only happen at the top-level as it relies on the order of Hooks to be always the same. If you wrap the Hook in a function, you can potentially invoke this function in many code locations disturbing the Hooks' order.
There is an internal list of “memory cells” associated with each component. They’re just JavaScript objects where we can put some data. When you call a Hook like useState(), it reads the current cell (or initializes it during the first render), and then moves the pointer to the next one. This is how multiple useState() calls each get independent local state. Link