I'm building my first Custom React Hook and am confused about what I think is a simple aspect of the code:
export const useFetch = (url, options) => {
const [data, setData] = useState();
const [loading, setLoading] = useState(true);
const { app } = useContext(AppContext);
console.log('** Inside useFetch: options = ', options);
useEffect(() => {
console.log('**** Inside useEffect: options = ', options);
const fetchData = async function() {
try {
setLoading(true);
const response = await axios.get(url, options);
if (response.status === 200) {
setData(response.data);
}
} catch (error) {
throw error;
} finally {
setLoading(false);
}
};
fetchData();
}, []);
return { loading, data };
};
I pass to useFetch two parameters: A url and a headers object that contains an AWS Cognito authorization key that looks like this: Authorization: eyJraWQiOiJVNW... (shortened for brevity)
When I do this the options object on does exist near within useFetch but within the useEffect construct it is empty. YET the url string is correctly populated in BOTH cases.
This makes no sense to me. Might anyone have an idea why this is occurring?
Below an implementation of your code showing that it works as expected.
The async/await has been converted to a Promise but should have the same behavior.
"Inside use fetch" is outputed 3 times:
on mount (useEffect(()=>..., [])
after first state change (setLoading(true))
after second state change (setLoading(false))
and "Inside use effect" is outputed 1 time on mount (useEffect(()=>..., [])
Since it doesn't work for you this way it could mean that when the component mounts, options is not available yet.
You confirm it when saying that when you put options as a dependency, useEffect is called two times with the first fetch failing (most likely because of options missing).
I'm pretty sure you will find the problem with options in the parents of the component using your custom hook.
const axios = {
get: (url, options) => {
return new Promise(resolve => setTimeout(() => resolve({ status: 200, data: 'Hello World' }), 2000));
}
};
const AppContext = React.createContext({ app: null });
const useFetch = (url, options) => {
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(true);
const { app } = React.useContext(AppContext);
console.log('** Inside useFetch: options = ', JSON.stringify(options));
React.useEffect(() => {
console.log('**** Inside useEffect: options = ', JSON.stringify(options));
const fetchData = function () {
setLoading(true);
const response = axios.get(url, options)
.then(response => {
if (response.status === 200) {
setData(response.data);
}
setLoading(false);
})
.catch(error => {
setLoading(false);
throw error;
});
};
fetchData();
}, []);
return { loading, data };
};
const App = ({url, options}) => {
const { loading, data } = useFetch(url, options);
return (
<div
style={{
display: 'flex', background: 'red',
fontSize: '20px', fontWeight: 'bold',
justifyContent: 'center', alignItems: 'center',
width: 300, height: 60, margin: 5
}}
>
{loading ? 'Loading...' : data}
</div>
);
};
ReactDOM.render(
<App
url="https://www.dummy-url.com"
options={{ headers: { Authorization: 'eyJraWQiOiJVNW...' } }}
/>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root" />
Related
I'm trying to abstract my fetch out to a hook in my expo react native app. The fetch must be able to do POST method. I started out by trying to use and then modify the useHook() effect found at https://usehooks-ts.com/react-hook/use-fetch. It caused too many re-renders. So in searching Stack Overflow I found this article that recommended that I do it as a function. Since, I anticipate almost every fetch request is going to be a POST and done with a submit, that looked like a good idea. But it is still giving me too many re-renders. Then I found this article that said to do it with a useCallBack. Still no success...
Here is the hook I'm trying to use:
import { useCallback, useState } from "react";
import { FetchQuery } from "../interfaces/FetchQuery";
interface State<T> {
data?: T;
error: Error | string | null;
fetchData: any;
}
const useFetch2 = <T = unknown,>(): State<T> => {
const [data, setData] = useState<T>();
const [error, setError] = useState<Error | null>(null);
const fetchData = useCallback(async (query: FetchQuery) => {
const { url, options } = query;
if (!url) return;
const response = await fetch(url, options);
const data = await response.json();
if (data.error) {
setData(undefined);
setError(data.error);
} else {
setData(data);
setError(null);
}
}, []);
return { fetchData, data, error };
};
export default useFetch2;
This is the component calling the code (right now it assigns all of the data to the error field so I can validate the data coming back):
export default function AdminLogin() {
// screen controls
const [err, setErr] = useState<Error | string | undefined>();
// form controls
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// fetch information
// const [fetchQuery, setFetchQuery] = useState<FetchQuery>({});
const { fetchData, data, error } = useFetch2<User>();
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
const handleSignIn = async () => {
const options = {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Cache-control": "no-cache",
},
body: JSON.stringify({ email, password }),
};
const url = BASE_API + "users/login";
// setFetchQuery({ url, options }); <-- attempted to use this with useEffect
fetchData({ url, options });
};
// useEffect(() => {
// fetchData();
// }, [fetchQuery]);
return (
<View style={styles.container}>
<Text style={styles.msg}>Maps can only be edited by Administers.</Text>
<View style={styles.controlGroup}>
<Text style={styles.UnPw}>Email</Text>
<TextInput
style={styles.txInput}
keyboardType="email-address"
placeholder="Email"
value={email}
onChangeText={(value) => setEmail(value)}
/>
</View>
<View style={styles.controlGroup}>
<Text style={styles.UnPw}>Password</Text>
<TextInput
style={styles.txInput}
maxLength={18}
placeholder="Password"
value={password}
onChangeText={(value) => setPassword(value)}
secureTextEntry
/>
</View>
<TouchableOpacity onPress={handleSignIn}>
<Text>Sign in</Text>
</TouchableOpacity>
{/* {status === "loading" && <Text>Loading...</Text>} */}
{err && (
<>
<Text style={styles.errText}>Error:</Text>
<Text style={styles.errText}>{err}</Text>
</>
)}
</View>
);
}
const styles = StyleSheet.create({});
Adding some console.log commands does show that states are continually updating after the fetch command, but I don't understand why nor how to fix it.
You should not update state directly in the component. This may cause the state to be updated on every render, which forces re-render and creates an infinite render loop.
Instead, state should be updated only in callbacks or useEffect.
Changes you have to make are convert following
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
to
useEffect(() => {
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
}, [error, data?.user])
Or, even better would be to remove err state and convert it to const variable like
// remove following
// const [err, setErr] = useState<Error | string | undefined>();
// ..some code
const { fetchData, data, error } = useFetch2<User>();
// err is not required to be a state
const err = error || JSON.strigify(data?.user);
You should remove this code which causes infinite re-renders:
if (error) {
setErr(error);
}
if (data?.user) {
setErr(JSON.stringify(data.user));
}
You should put it to useEffect, something like this:
useEffect(() => {
setErr(error);
}, [error])
useEffect(() => {
setErr(JSON.stringify(data.user));
}, [data?.user])
Because that code updates state on every render.
I was working on a functionality which involved fetching the data from an API and rendering the components accordingly.
index.js
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import StarRating from './components/StarRating';
import {
API_URL,
API_TIMEOUT,
} from '../../../../someLoc';
export async function fetchWithTimeout(resource, options = {}) {
const { timeout } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
...options,
signal: controller.signal,
});
clearTimeout(id);
return response;
}
const UserRating = ({ productId }) => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState({});
const [error, setError] = useState(false);
useEffect(
() => {
const fetchReviewsNRatings = async (bvUrl, bvTimeout) => {
const url = `${bvUrl}:${productId}`;
const response = await fetchWithTimeout(url, { timeout: bvTimeout });
if (response.ok) {
const ratingData = await response.json();
setData(ratingData);
setLoading(false);
} else {
setError(true);
setLoading(false);
}
};
fetchReviewsNRatings(API_TIMEOUT);
},
[productId],
);
// I didn't know how to test this branch if I simply returned null. so wrapped null inside of div.
// MY INTENT HERE: if(loading) return null;
if (loading) return <div className="test-class">{null}</div>;
// destructuring the response to get the data once loading is complete.
const {
Results: [
{
ABC: {
DEF: { GHI, JKL },
},
},
],
} = data;
const numReviews = +GHI;
const value = JKL.toFixed(1);
return !error && <StarRating value={value} numReviews={numReviews} />;
};
UserRating.propTypes = {
productId: PropTypes.string,
};
export default UserRating;
index.test.js
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import UserRating from '..';
import { PRODUCT_ID } from '../../../../../someLoc';
describe('Testing for <UserRating /> component', () => {
const data = {
Results: [
{
ABC: {
DEF: {
GHI: 4.567,
JKL: 1103,
},
},
},
],
};
const productId = PRODUCT_ID;
let component = null;
beforeEach(() => {
global.fetchWithTimeout = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
data,
json: () => data,
}),
);
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
data,
json: () => data,
}),
);
component = act(() => mount(<UserRating productId={productId} />));
});
it('should call fetch', async () => {
expect(component.exists()).toBeTruthy();
// Testting for loading state.
expect(component.find('.test-class').exists()).toBe(true);
});
What's working as expected:
The functionality that is expected.
The above test which I managed to write is passing for win-win scenario i.e. when the data is loaded and fetched successfully.
MY QUESTION:
a. I want to write a unit test in such a way so as to test what happens if there is some error from server side(5xx) or from client side(4xx).
b. What else can I improve in my passing test scenario?
What I have tried:
describe('Testing for <UserRating /> component for failed response', () => {
const data = { message: '500: Internal Server Error' };
const error = true;
const productId = PRODUCT_ID;
let component = null;
beforeEach(() => {
global.fetchWithTimeout = jest.fn(() =>
Promise.reject(
new Error({
ok: false,
status: 500,
data,
json: () => data,
}),
),
);
global.fetch = jest.fn(() =>
Promise.reject(
new Error({
ok: false,
status: 500,
data,
json: () => data,
}),
),
);
component = act(() => mount(<UserRating productId={productId} />));
});
it('should not fetch', async () => {
expect(component.exists()).toBeFalsy();
expect(component.find('.test-class').exists()).toBe(true);
console.debug(component);
});
});
When I tried the above code it is breaking the win-win scenario as well with the error message cannot read property exists for null. I am relatively new to enzyme and facing issues while using enzyme for writing tests for hooks. I know that React testing library is preferrable but my organization is yet to shift onto that. I've to write fail case as well since the coverage for branches is poor. I'll be highly obliged for any help. Thanks
REFFERED LINKS and Questions:
how-to-test-async-data-fetching-react-component-using-jest-and-enzyme
Testing hooks with mount
Should a Promise.reject message be wrapped in Error?
I have laravel in server side, that can show api with entering this url: http://localhost:8000/api/cabangs, and show this (the data as example):
[
{
"id":2,
"nm_cabang":"zxcvb",
"deskripsi":"poiuyt",
"created_at":"2020-08-08T05:25:31.000000Z",
"updated_at":"2020-08-08T05:29:23.000000Z"
},
{
"id":3,
"nm_cabang":"asdfg",
"deskripsi":"qwerty",
"created_at":"2020-08-08T05:28:26.000000Z",
"updated_at":"2020-08-08T05:28:26.000000Z"
}
]
I want to only display nm_cabang and deskripsi. if it's possible, using react hooks. thanks
You could start with something like this.
I'm assuming this is for the web so I used span and div.
const App: React.FC = () => {
const [data, setData] = React.useState();
const [isLoading, setLoading] = React.useState(true);
React.useEffect(() => {
const fetchData = async () => {
try {
const result = await fetch('http://localhost:8000/api/cabangs');
if (result.ok)
{
let json = await result.json();
setData(json);
}
} catch (e) {
//error
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (isLoading) {
return <span>Loading...</span>;
}
return (
<div>
{
data ? (
data.map((item) => {
return <span>{`${item.nm_cabang} ${item.deskripsi}`}</span>;
})
) : (
<></>
)
}
</div>
);
};
Use a library like axios or fetch, you only need to make the http request to the url and use the data you need.
https://github.com/axios/axios
https://javascript.info/fetch
I like axios
I'm newbie in React but I'm developing an app which loads some data from the server when user open the app. App.js render this AllEvents.js component:
const AllEvents = function ({ id, go, fetchedUser }) {
const [popout, setPopout] = useState(<ScreenSpinner className="preloader" size="large" />)
const [events, setEvents] = useState([])
const [searchQuery, setSearchQuery] = useState('')
const [pageNumber, setPageNumber] = useState(1)
useEvents(setEvents, setPopout) // get events on the main page
useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber)
// for ajax pagination
const handleSearch = (searchQuery) => {
setSearchQuery(searchQuery)
setPageNumber(1)
}
return(
<Panel id={id}>
<PanelHeader>Events around you</PanelHeader>
<FixedLayout vertical="top">
<Search onChange={handleSearch} />
</FixedLayout>
{popout}
{
<List id="event-list">
{
events.length > 0
?
events.map((event, i) => <EventListItem key={event.id} id={event.id} title={event.title} />)
:
<InfoMessages type="no-events" />
}
</List>
}
</Panel>
)
}
export default AllEvents
useEvents() is a custom hook in EventServerHooks.js file. EventServerHooks is designed for incapsulating different ajax requests. (Like a helper file to make AllEvents.js cleaner) Here it is:
function useEvents(setEvents, setPopout) {
useEffect(() => {
axios.get("https://server.ru/events")
.then(
(response) => {
console.log(response)
console.log(new Date())
setEvents(response.data.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
)
}, [])
return null
}
function useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber) {
useEffect(() => {
setPopout(<ScreenSpinner className="preloader" size="large" />)
let cancel
axios({
method: 'GET',
url: "https://server.ru/events",
params: {q: searchQuery, page: pageNumber},
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(
(response) => {
setEvents(response.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
).catch(
e => {
if (axios.isCancel(e)) return
}
)
return () => cancel()
}, [searchQuery, pageNumber])
return null
}
export { useEvents, useSearchedEvents }
And here is the small component InfoMessages from the first code listing, which display message "No results" if events array is empty:
const InfoMessages = props => {
switch (props.type) {
case 'no-events':
{console.log(new Date())}
return <Div className="no-events">No results :(</Div>
default:
return ''
}
}
export default InfoMessages
So my problem is that events periodically loads and periodically don't after app opened. As you can see in the code I put console log in useEvents() and in InfoMessages so when it's displayed it looks like this:
logs if events are displayed, and the app itself
And if it's not displayed it looks like this: logs if events are not displayed, and the app itself
I must note that data from the server is loaded perfectly in both cases, so I have totally no idea why it behaves differently with the same code. What am I missing?
Do not pass a hook to a custom hook: custom hooks are supposed to be decoupled from a specific component and possibly reused. In addition, your custom hooks return always null and that's wrong. But your code is pretty easy to fix.
In your main component you can fetch data with a custom hook and also get the loading state like this, for example:
function Events () {
const [events, loadingEvents] = useEvents([])
return loadingEvents ? <EventsSpinner /> : <div>{events.map(e => <Event key={e.id} title={e.title} />}</div>
}
In your custom hook you should return the internal state. For example:
function useEvents(initialState) {
const [events, setEvents] = useState(initialState)
const [loading, setLoading] = useState(true)
useEffect(function() {
axios.get("https://server.ru/events")
.then(
(res) => {
setEvents(res.data)
setLoading(false)
}
)
}, [])
return [events, loading]
}
In this example, the custom hook returns an array because we need two values, but you could also return an object with two key/value pairs. Or a simple variable (for example only the events array, if you didn't want the loading state), then use it like this:
const events = useEvents([])
This is another example that you can use, creating a custom hook that performs the task of fetching the information
export const useFetch = (_url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(true);
useEffect(function() {
setLoading('procesando...');
setData(null);
setError(null);
const source = axios.CancelToken.source();
setTimeout( () => {
axios.get( _url,{cancelToken: source.token})
.then(
(res) => {
setLoading(false);
console.log(res.data);
//setData(res);
res.data && setData(res.data);
// res.content && setData(res.content);
})
.catch(err =>{
setLoading(false);
setError('si un error ocurre...');
})
},1000)
return ()=>{
source.cancel();
}
}, [_url])
https://codesandbox.io/s/react-hooks-usefetch-cniul
Please see above url for a very simplified version of my code.
I want to be able to refetch data from an API with my hook, within an interval (basically poll an endpoint for data).
What I want is to be able to just call something like refetch (as I've shown in the code as a comment), which would essentially just call fetchData again and update state with the response accordingly.
What's the best way to go about this? The only way I can think of is to add a checker variable in the hook which would be some sort of uuid (Math.random() maybe), return setChecker as what is refetch and just add checker to the array as 2nd useEffect argument to control rerendering. So whenever you call refetch it calls setChecker which updates the random number (checker) and then the function runs again.
Obviously this sounds "hacky", there must be a nicer way of doing it - any ideas?
If you want to have a constant poll going, I think you can move the setInterval() into the hook like so:
function useFetch() {
const [data, setDataState] = useState(null);
const [loading, setLoadingState] = useState(true);
useEffect(() => {
function fetchData() {
setLoadingState(true);
fetch(url)
.then(j => j.json())
.then(data => {
setDataState(data);
setLoadingState(false);
});
}
const interval = setInterval(() => {
fetchData();
}, 5000);
fetchData();
return () => clearInterval(interval);
}, []);
return [
{
data,
loading
}
];
}
Remember to include the return () => clearInterval(interval); so the hook is cleaned up correctly.
import React, { useEffect, useState, useCallback } from "react";
import ReactDOM from "react-dom";
const url = "https://api.etilbudsavis.dk/v2/dealerfront?country_id=DK";
function useFetch() {
const [data, setDataState] = useState(null);
const [loading, setLoadingState] = useState(true);
const refetch = useCallback(() => {
function fetchData() {
console.log("fetch");
setLoadingState(true);
fetch(url)
.then(j => j.json())
.then(data => {
setDataState(data);
setLoadingState(false);
});
}
fetchData();
}, []);
return [
{
data,
loading
},
refetch
// fetchData <- somehow return ability to call fetchData function...
];
}
function App() {
const [
{ data, loading },
refetch
// refetch
] = useFetch();
useEffect(() => {
const id = setInterval(() => {
// Use the refetch here...
refetch();
}, 5000);
return () => {
clearInterval(id);
};
}, [refetch]);
if (loading) return <h1>Loading</h1>;
return (
<>
<button onClick={refetch}>Refetch</button>
<code style={{ display: "block" }}>
<pre>{JSON.stringify(data[0], null, 2)}</pre>
</code>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Maybe the following will work, it needs some adjustments to useFetch but you can still call it normally in other places.
//maybe you can pass url as well so you can use
// it with other components and urls
function useFetch(refresh) {
//code removed
useEffect(() => {
//code removed
}, [refresh]);
//code removed
}
const [refresh, setRefresh] = useState({});
const [{ data, loading }] = useFetch(refresh);
useEffect(() => {
const interval = setInterval(
() => setRefresh({}), //forces re render
5000
);
return () => clearInterval(interval); //clean up
});
Simple answer to question:
export default function App() {
const [entities, setEntities] = useState();
const [loading, setLoadingState] = useState(true);
const getEntities = () => {
setLoadingState(true);
//Changet the URL with your own
fetch("http://google.com", {
method: "GET",
})
.then((data) => data.json())
.then((resp) => {
setEntities(resp);
setLoadingState(false);
});
};
useEffect(() => {
const interval = setInterval(() => {
getEntities();
}, 5000);
return () => clearInterval(interval);
}, []);
}