This is my component to be tested.
// WelcomePgaeContainer.jsx
import React, { useEffect, useState } from "react";
import WelcomePage from "./WelcomePage";
import api from "../../../api";
import apiUrl from "../../../apiUrls";
import { Loader } from "../../../shared/components";
const WelComePageContainer = () => {
const [welcomeData, updateWelcomeData] = useState({});
useEffect(() => {
api.get(apiUrl.WELCOME_PAGE.GET_WELCOME_DATA).then(({ data }) => {
updateWelcomeData(data);
});
}, []);
return (
<div>
{Object.keys(welcomeData).length ? (
<WelcomePage />
) : (
<div data-testid="welcome-page-loader" className={styles.loaderContainer}>
<Loader size={5} />
</div>
)}
</div>
);
};
export default WelComePageContainer;
// api.js
import axios from "axios";
const api = axios.create();
api.defaults.headers.post["Content-Type"] = "application/json";
api.interceptors.request.use(
config => {
config.withCredentials = false;
return config;
},
error => Promise.reject(error)
);
api.interceptors.response.use(
response => response,
error => {
return Promise.reject(error.response);
}
);
export default API;
// Welcome.test.js
it("test welcome page is loaded and async data call is made once", async () => {
jest.doMock("../../src/api", () => {
const mock = new MockAdapter(axios);
mock.onGet(apiUrl.WELCOME_PAGE.GET_WELCOME_DATA).reply(200, WELCOME_DATA);
});
const history = createMemoryHistory();
const { findByTestId } = render(
<Router history={history}>
<WelComePageContainer />
</Router>
);
const welcomeViewElement = await findByTestId("welcome-data-container");
expect(welcomeViewElement).toBeTruthy();
});
I am trying to mock import api from "../../../api"; of WelcomePgaeContainer.jsx in my test file. However, when calling from Welcome.test.js the actual is getting invoked rather than the mocked one.
Related
I'm trying to pass data to the component, that i received from the API. I am using 'createAsyncThunk' to save it in the state, than when trying to get my data, get error "undefined". I understand that it happens, cause it's need some time to get data from API, but how i can force component "waiting"? What is wrong with my code?
Here is my code:
Step 1: Gettind data from API, filtered it and push it in state.
import { generateRandom } from "../helpers/randomInt";
const API_URL = "https://akabab.github.io/superhero-api/api/all.json";
export const fetchHeroes = createAsyncThunk(
"data_slice/fetchHeroes",
async function (_, { rejectWithValue }) {
try {
const res = await fetch(API_URL);
if (!res.ok) {
throw new Error("Could not fetch cart data!");
}
const data = await res.json();
const marvel_heroes = data.filter(
(item) => item.biography.publisher == "Marvel Comics"
);
const dark_horse_heroes = data.filter(
(item) => item.biography.publisher == "Dark Horse Comics"
);
const dc_heroes = data.filter(
(item) => item.biography.publisher == "DC Comics"
);
const filtered_data = [
...marvel_heroes,
...dark_horse_heroes,
...dc_heroes,
];
const heroesData = [];
for (let index = 0; index < 49; index++) {
const item = filtered_data[generateRandom(0, 439)];
heroesData.push(item);
}
const main_data = [filtered_data, heroesData];
return main_data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const heroesSlice = createSlice({
name: "data_slice",
initialState: { heroes_data: [], isLoading: null, error: null },
extraReducers: {
[fetchHeroes.pending]: (state) => {
state.isLoading = true;
},
[fetchHeroes.fulfilled]: (state, action) => {
state.heroes_data = action.payload;
state.isLoading = false;
},
[fetchHeroes.rejected]: (state) => {
state.isLoading = false;
state.error = "Something go wrong!";
alert("aaa");
},
},
});
export default heroesSlice;
Step 2: Firing (using dispatch) fetch function "fetchHeroes" in 'App.js' with 'UseEffect' to get data when app starting
import { Fragment, useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { fetchHeroes } from "./store/heroes-slice";
import { Routes, Route } from "react-router-dom";
import Main from "./pages/Main";
import Heroes from "./pages/Heroes";
import Hero_page from "./pages/Hero_page";
import LoginModal from "./components/LoginModal";
import RegisterModal from "./components/RegisterModal";
function App() {
const [scrollY, setScrollY] = useState(0);
const isLoginModal = useSelector((state) => state.modal.isLoginModal);
const isRegisterModal = useSelector((state) => state.modal.isRegisterModal);
const dispatch = useDispatch();
function logit() {
setScrollY(window.scrollY);
console.log(new Date().getTime());
}
useEffect(() => {
function watchScroll() {
window.addEventListener("scroll", logit);
}
watchScroll();
return () => {
window.removeEventListener("scroll", logit);
};
});
useEffect(() => {
dispatch(fetchHeroes());
}, [dispatch]);
return (
<Fragment>
{isRegisterModal && <RegisterModal></RegisterModal>}
{isLoginModal && <LoginModal></LoginModal>}
<Routes>
<Route path="/" element={<Main />} />
<Route path="/heroes" exact element={<Heroes scroll={scrollY} />} />
<Route path="/heroes/:heroId" element={<Hero_page />}></Route>
</Routes>
</Fragment>
);
}
export default App;
Step 3: I am trying to recieve data from state(heroes_fetched_data) using 'usSelector', but when trying parce it through 'map', get error 'undefined'
import classes from "./Heroes.module.css";
import Header from "../components/Header";
import Footer from "../components/Footer.js";
import Hero_card from "../components/Hero_card";
import { useSelector } from "react-redux";
import { Link } from "react-router-dom";
export default function Heroes(props) {
const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
const loadingStatus = useSelector((state) => state.heroes.isLoading);
console.log(heroes_fetched_data);
const heroes_cards = heroes_fetched_data[1].map((item, i) => (
<Link to={`/heroes/${item.id}`} key={item.id + Math.random()}>
<Hero_card
key={i}
img={item.images.lg}
name={item.name}
publisher={item.biography.publisher}
/>
</Link>
));
return (
<div className={classes.main}>
<Header scroll={props.scroll} />
{!loadingStatus && (
<section className={classes.heroes}>
<ul className={classes.ully} id="heroes">
{heroes_cards}
</ul>
</section>
)}
{loadingStatus && <p>Loading...</p>}
<Footer />
</div>
);
}
Because the fetch is asynchronous, you cannot assume that heroes_fetched_data inside your Heroes component will have the data when the component first renders. You need to check whether this data is present before attempting to use it. If it's not yet present, the component should render an alternate "loading" state. When the fetch completes, your component should re-render automatically, at which point heroes_fetched_data will have the data you want and you can proceed.
Roughly, you want something like this pseudocode:
export default function Heroes(props) {
const heroes_fetched_data = useSelector((state) => state.heroes.heroes_data);
const loadingStatus = useSelector((state) => state.heroes.isLoading);
if (!heroes_fetched_data) {
return <p>{loadingStatus}</p>;
}
const heroes_cards = heroes_fetched_data[1].map((item, i) => (
// ...
);
// proceed as normal
}
setAcessKeys is not updating the state immediately even though the data is available at that point which I know through the console.log(data) or by passing the array manually. I realized that without useEffect, it renders multiple times and the console.log(accessKeys) returns data from the third run going.
import { useState, useEffect } from "react";
import axios from "axios";
import AccessKey from "./AccessKey";
import { toast } from "react-toastify";
import { useNavigate } from "react-router-dom";
import "./AccessKey.module.css";
const AccessKeys = () => {
const [accessKeys, setAccessKeys] = useState([]);
const navigate = useNavigate();
useEffect(() => {
const getAccessKeys = async () => {
try {
let token = localStorage.getItem("auth");
const response = await axios.get(
"http://localhost:5000/api/keys/user",
{
headers: {
authorization: `Bearer ${token}`,
},
}
);
const data = response.data;
console.log(data); // [{...}, {...}]
setAccessKeys((prevKeys) => [...prevKeys, ...data]);
console.log(accessKeys); // []
} catch (error) {
navigate("/");
toast.error(error.response.data.message);
}
};
getAccessKeys();
}, [navigate, accessKeys]);
return (
<>
{accessKeys.length > 0 ? (
<main>
{accessKeys.map((accessKey) => (
<AccessKey key={accessKey._id} acesskey={accessKey} />
))}
</main>
) : (
<h4>You do not have any Access Keys at the moment</h4>
)}
</>
);
};
export default AccessKeys;
I have component that use thunk action.
And inside the component, I have an asynchronous execution that, after processing, returns the username to the markup, how to lock the result of such an execution
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from "react-redux";
import setTestData from "../redux/asyncServices/testService";
function TestPage() {
const dispatch = useDispatch()
const user = useSelector((state) => state.testReducer.user)
const loading = useSelector((state) => state.testReducer.loading)
useEffect(() => {
dispatch(setTestData())
}, [])
return (
<div className='users_wrapper'>
<div className='container'>
<div className='users_content'>
<div className='title'>
<h1>Test</h1>
</div>
{
!loading ? <h1>{user.name}</h1> : null
}
</div>
</div>
</div>
)
}
export default TestPage
Async action
import { createAsyncThunk } from '#reduxjs/toolkit'
import db from '../../indexedDB/db'
import '../../indexedDB/db.timesheetHooks'
const setTestData = createAsyncThunk(
'setTestData',
async () => {
const user = await db.loggedUser.orderBy('id').last()
return {user}
},
)
And code where I try to mock function result, but something went wrong.I understand how mock simple function in jest, but how mock in this case
jest.mock('../redux/asyncServices/testService', () => {
return {
setTestData: () => ({type: "setTestData/fulfilled", payload: {
user: {name: 'Loaded user name'}
}}),
};
});
describe('Timesheet Menu page tests', () => {
beforeEach(async () => {
matchMedia = new MatchMediaMock()
})
afterEach(() => {
matchMedia.clear()
})
test('Component renders correctly', async () => {
const testFn = require('../pages/TestPage');
jest.spyOn(testFn, 'setTestData').mockReturnValue('c');
await waitFor(() => {
renderWithRedux(<TestPage/>, {initialState})
})
expect(screen.getByText('Test')).toBeInTheDocument()
expect(screen.getByText('Loaded user name')).toBeInTheDocument()
})
})
I'm using socket.io along with react for a project. Here's my component
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { io } from 'socket.io-client';
import Button from '../../components/Button';
import { IProject } from '../../interfaces/projects';
import { IRun } from '../../interfaces/runs';
const socket = io(process.env.REACT_APP_SERVER_URL);
export default function RunAll() {
const { search } = useLocation();
// API State
const [project, setProject] = useState<IProject | undefined>(undefined);
const [runs, setRuns] = useState<IRun[]>([]);
// Query Params
const queryParams = new URLSearchParams(search);
const projectId = queryParams.get('projectId')!;
// Get Project
useEffect(() => {
(async () => {
const { data: project } = await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/projects/${projectId}`);
setProject(project.data);
})();
}, [projectId]);
// Clear socket
useEffect(() => () => {
socket.close();
});
const runAllTests = async () => {
project?.tests.forEach((test) => {
console.log(test);
socket.emit('create run', { projectId, testId: test.id, url: process.env.REACT_APP_SERVER_URL });
});
socket.on('created run', (run: IRun) => {
console.log(run);
setRuns([...runs, run]);
});
};
console.log(runs);
const renderHeader = () => (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', alignItems: 'center' }} className='mb-3'>
<h1 className='heading-primary mt-auto mb-auto'>Run Overview</h1>
<Button onClick={runAllTests}>Run All Tests</Button>
</div>
);
return (
<main>
{renderHeader()}
{runs?.map((run) => (
<div>{run.id}</div>
))}
</main>
);
}
When the button is click and runAllTests() is called, i can see the console.log(test) in the console and my server logs also show me that it has received the socket.emit('create run'). And when the server responds with socket.on('created run'), I can see the value of the created run. However, only the 2nd run(in case of the length of project.tests being 2), only the last run is being added to the state.
What am I missing here? Please help me out! Thanks in advance!
socket.on('created run', (run: IRun) => { console.log(run); setRuns([...runs, run]); });
use this code in useEffect and make sure it only runs once. Following should work for you.
import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { io } from 'socket.io-client';
import Button from '../../components/Button';
import { IProject } from '../../interfaces/projects';
import { IRun } from '../../interfaces/runs';
const socket = io(process.env.REACT_APP_SERVER_URL);
export default function RunAll() {
const { search } = useLocation();
// API State
const [project, setProject] = useState<IProject | undefined>(undefined);
const [runs, setRuns] = useState<IRun[]>([]);
// Query Params
const queryParams = new URLSearchParams(search);
const projectId = queryParams.get('projectId')!;
// Get Project
useEffect(() => {
(async () => {
const { data: project } = await axios.get(`${process.env.REACT_APP_SERVER_URL}/api/projects/${projectId}`);
setProject(project.data);
})();
}, [projectId]);
// Clear socket
useEffect(() => () => {
socket.on('created run', (run: IRun) => {
console.log(run);
setRuns([...runs, run]);
});
return function cleanup () {
socket.close();
}
},[]);
const runAllTests = async () => {
project?.tests.forEach((test) => {
console.log(test);
socket.emit('create run', { projectId, testId: test.id, url: process.env.REACT_APP_SERVER_URL });
});
};
console.log(runs);
const renderHeader = () => (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', alignItems: 'center' }} className='mb-3'>
<h1 className='heading-primary mt-auto mb-auto'>Run Overview</h1>
<Button onClick={runAllTests}>Run All Tests</Button>
</div>
);
return (
<main>
{renderHeader()}
{runs?.map((run) => (
<div>{run.id}</div>
))}
</main>
);
}
I'm obviously not cleaning up correctly and cancelling the axios GET request the way I should be. On my local, I get a warning that says
Can't perform a React state update on an unmounted component. This is
a no-op, but it indicates a memory leak in your application. To fix,
cancel all subscriptions and asynchronous tasks in a useEffect cleanup
function.
On stackblitz, my code works, but for some reason I can't click the button to show the error. It just always shows the returned data.
https://codesandbox.io/s/8x5lzjmwl8
Please review my code and find my flaw.
useAxiosFetch.js
import {useState, useEffect} from 'react'
import axios from 'axios'
const useAxiosFetch = url => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(true)
let source = axios.CancelToken.source()
useEffect(() => {
try {
setLoading(true)
const promise = axios
.get(url, {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log(`request cancelled:${thrown.message}`)
} else {
console.log('another error happened')
}
})
.then(a => {
setData(a)
setLoading(false)
})
} catch (e) {
setData(null)
setError(e)
}
if (source) {
console.log('source defined')
} else {
console.log('source NOT defined')
}
return function () {
console.log('cleanup of useAxiosFetch called')
if (source) {
console.log('source in cleanup exists')
} else {
source.log('source in cleanup DOES NOT exist')
}
source.cancel('Cancelling in cleanup')
}
}, [])
return {data, loading, error}
}
export default useAxiosFetch
index.js
import React from 'react';
import useAxiosFetch from './useAxiosFetch1';
const index = () => {
const url = "http://www.fakeresponse.com/api/?sleep=5&data={%22Hello%22:%22World%22}";
const {data,loading} = useAxiosFetch(url);
if (loading) {
return (
<div>Loading...<br/>
<button onClick={() => {
window.location = "/awayfrom here";
}} >switch away</button>
</div>
);
} else {
return <div>{JSON.stringify(data)}xx</div>
}
};
export default index;
Here is the final code with everything working in case someone else comes back.
import {useState, useEffect} from "react";
import axios, {AxiosResponse} from "axios";
const useAxiosFetch = (url: string, timeout?: number) => {
const [data, setData] = useState<AxiosResponse | null>(null);
const [error, setError] = useState(false);
const [errorMessage, setErrorMessage] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let unmounted = false;
let source = axios.CancelToken.source();
axios.get(url, {
cancelToken: source.token,
timeout: timeout
})
.then(a => {
if (!unmounted) {
// #ts-ignore
setData(a.data);
setLoading(false);
}
}).catch(function (e) {
if (!unmounted) {
setError(true);
setErrorMessage(e.message);
setLoading(false);
if (axios.isCancel(e)) {
console.log(`request cancelled:${e.message}`);
} else {
console.log("another error happened:" + e.message);
}
}
});
return function () {
unmounted = true;
source.cancel("Cancelling in cleanup");
};
}, [url, timeout]);
return {data, loading, error, errorMessage};
};
export default useAxiosFetch;
Based on Axios documentation cancelToken is deprecated and starting from v0.22.0 Axios supports AbortController to cancel requests in fetch API way:
//...
React.useEffect(() => {
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
return () => {
controller.abort();
};
}, []);
//...
The issue in your case is that on a fast network the requests results in a response quickly and it doesn't allow you to click the button. On a throttled network which you can achieve via ChromeDevTools, you can visualise this behaviour correctly
Secondly, when you try to navigate away using window.location.href = 'away link' react doesn't have a change to trigger/execute the component cleanup and hence the cleanup function of useEffect won't be triggered.
Making use of Router works
import React from 'react'
import ReactDOM from 'react-dom'
import {BrowserRouter as Router, Switch, Route} from 'react-router-dom'
import useAxiosFetch from './useAxiosFetch'
function App(props) {
const url = 'https://www.siliconvalley-codecamp.com/rest/session/arrayonly'
const {data, loading} = useAxiosFetch(url)
// setTimeout(() => {
// window.location.href = 'https://www.google.com/';
// }, 1000)
if (loading) {
return (
<div>
Loading...
<br />
<button
onClick={() => {
props.history.push('/home')
}}
>
switch away
</button>
</div>
)
} else {
return <div>{JSON.stringify(data)}</div>
}
}
ReactDOM.render(
<Router>
<Switch>
<Route path="/home" render={() => <div>Hello</div>} />
<Route path="/" component={App} />
</Switch>
</Router>,
document.getElementById('root'),
)
You can check the demo working correctly on a slow network
Fully cancellable routines example, where you don't need any CancelToken at all (Play with it here):
import React, { useState } from "react";
import { useAsyncEffect, E_REASON_UNMOUNTED } from "use-async-effect2";
import { CanceledError } from "c-promise2";
import cpAxios from "cp-axios"; // cancellable axios wrapper
export default function TestComponent(props) {
const [text, setText] = useState("");
const cancel = useAsyncEffect(
function* () {
console.log("mount");
this.timeout(props.timeout);
try {
setText("fetching...");
const response = yield cpAxios(props.url);
setText(`Success: ${JSON.stringify(response.data)}`);
} catch (err) {
CanceledError.rethrow(err, E_REASON_UNMOUNTED); //passthrough
setText(`Failed: ${err}`);
}
return () => {
console.log("unmount");
};
},
[props.url]
);
return (
<div className="component">
<div className="caption">useAsyncEffect demo:</div>
<div>{text}</div>
<button onClick={cancel}>Abort</button>
</div>
);
}
This is how I do it, I think it is much simpler than the other answers here:
import React, { Component } from "react";
import axios from "axios";
export class Example extends Component {
_isMounted = false;
componentDidMount() {
this._isMounted = true;
axios.get("/data").then((res) => {
if (this._isMounted && res.status === 200) {
// Do what you need to do
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
return <div></div>;
}
}
export default Example;