How to fetch a json object while using hooks in reactjs - javascript

I'm trying get my head around hooks in react, seemed pretty easy until I tried using fetch to get a json Object, the code I used is below
const [row, setRow] = useState({
response: null,
error: false,
loading: true
});
useEffect(() => {
setRow({...row, error: null, loading: true});
fetch("/todo?page[number]=1&page[size]=100000")
.then(async (response) => {
const data = await response.json();
setRow({
response: data,
error: !response.ok,
loading: false,
});
console.log('response', data);
console.log('Data fetched', row);
})
.catch((err) => {
setRow({
response: {status: "network_failure"},
error: true,
loading: false,
})
console.log('err' + err);
});
}, []);
Which produces the following result:
If could give some hints I would be really be appreciated, Thanks.

Ok, you've mixed and matched synchronous and asynchronous programming in the same block, so lets simplify and just take the complete asynchronous approach using the async/await declarations. It immediately transformers your useEffect into this:
useEffect(async () => {
try {
const response = await fetch("/todo?page[number]=1&page[size]=100000");
const data = await response.json();
setRow({
response: data,
error: !response.ok,
loading: false
});
} catch (e) {
setRow({
response: { status: "network_failure" },
error: true,
loading: false
});
console.error(e);
}
}, []);
As for your original question, it was fetching and returning the JSON as you requested, see the screenshot output. If you want to render the todo list, which is what I presume you want to do, change the setState to this:
const [loading, setLoading] = useState(true);
const [todos, setTodos] = useState([]);
const [error, setError] = useState(undefined);
Then we update the useEffect...
useEffect(async () => {
try {
const response = await fetch("/todo?page[number]=1&page[size]=100000");
const data = await response.json();
setTodos(data);
setLoading(false);
setError(undefined);
} catch (e) {
setLoading(false);
setError(e.message);
setTodos([]);
}
}, []);
Then we add rendering...
const MyComponent = () => {
// ... useEffect & useState code
if (error) {
return (<p>There was an error loading todos, error: {error}</p>);
}
return (
<div>{todos.map(todo => <p key={todo.id}>{todo.title}{todo.completed ? " (completed)" : ""})}</p></div>
);
};
Working example on codepen using https://jsonplaceholder.typicode.com (it takes a while to load)
https://codepen.io/jmitchell38488/pen/GRoqeJp

Related

Promise.all in useEffect hook in React

I'm dealing with a use case where two API calls are made one after the other. The second API call is made by taking a value from the first API call response. I was able to use Promise.all with axios in my React application.
Is there a better way to call the APIs in chain in useEffect hook? I'm open to suggestions or recommendations. Could anyone please help?
useEffect(async () => {
const getFirstResponse = async () => {
try {
return await axios.get('http://first-api', {
params: { carId: id },
});
} catch (error) {
return error;
}
};
const firstResponse = await getFirstResponse();
const getSecondResponse = async () => {
try {
return await axios.get('http://second-api', {
params: { carName: firstResponse.data?.carName },
});
} catch (error) {
return error;
}
};
const secondResponse = await getSecondResponse();
Promise.all([firstResponse, secondResponse])
.then(function (values) {
console.log(`values`, values);
})
.catch(function (err) {
console.log(err);
});
}, []);
Promise.all is completely superfluous here.
It is a tool for handling promises that are running in parallel not in series.
The argument you pass to it should be an array of promises. firstResponse and secondResponse are the values that have been unwrapped from promises by await.
Just use firstResponse and secondResponse directly.
const secondResponse = await getSecondResponse();
console.log([firstResponse, secondResponse]);
For that matter, creating the nested async functions and having multiple try/catch blocks that do the same thing just makes the code harder to read.
You can reduce the whole thing down to:
useEffect(() => {
const asyncFunction = async () => {
try {
const firstResponse = await axios.get('http://first-api', {
params: { carId: id },
});
const secondResponse = await axios.get('http://second-api', {
params: { carName: firstResponse.data?.carName },
});
console.log(`values`, [firstResponse, secondResponse]);
} catch (error) {
return error;
}
}
asyncFunction();
}, []);
another possible way to get the result using Promise.all, but I like Quentin`s answer
try {
const responses = await Promise.all([body1, body2].map(async body => {
return await axios.get(body.endpoint, body.params);
}))
} catch(error) {
return error;
}
How about you write a custom hook that handles loading and error state so you don't have to rewrite it for each component?
Avoid defining complex functions inside of useEffect
Separate api calls into individual functions. This makes them reusable in other areas of your app
Don't return errors on the resolved branch of your Promise. Instead let them reject and allow the caller to handle appropriately
Let's look at MyComponent. All complexity is removed and the component is only concerned with the three possible states of the asynchronous call -
// MyComponent.js
import { useAsync } from "./hooks"
import { fetchCarWithDetails } from "./api"
function MyComponent({ carId }) {
const {loading, error, result} =
useAsync(fetchCarWithDetails, [carId]) // reusable hook
// loading...
if (loading)
return <p>Loading...</p>
// error...
if (error)
return <p>Error: {error.message}</p>
// result...
return <pre>
{JSON.stringify(result, null, 2)}
</pre>
}
Our reusable api functions are defined in our api module -
// api.js
import axios from "axios"
function fetchCar(carId) {
return axios
.get('http://first-api', {params: {carId}})
.then(r => r.data)
}
function fetchDetails(carName) {
return axios
.get('http://second-api', {params: {carName}})
.then(r => r.data)
}
async function fetchCarWithDetails(carId) {
const car = await fetchCar(carId)
const details = await fetchDetails(car.carName)
return { car, details }
}
export { fetchCar, fetchDetails, fetchCarWithDetails }
Reusable hooks are defined in our hooks module -
// hooks.js
function useAsync(f, [_0, _1, _2, _3, _4]) {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
setLoading(true)
Promise.resolve(f(_0, _1, _2, _3, _4))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, [f, _0, _1, _2, _3, _4])
return {loading, error, result}
}
export { useAsync }

Cleaning up a useEffect warning with useReducer,

I keep getting these warnings:
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
For some of my useEffects that pull data from an API with the help of my useReducer:
export default function HomeBucketsExample(props) {
const {mobileView} = props
const [allDemoBuckets, dispatchAllBuckets] = useReducer(reducer, initialStateAllBuckets)
const ListLoading = LoadingComponent(HomeBucketLists);
useEffect(() =>
{
getAllDemoBuckets(dispatchAllBuckets);
}, [])
return (
<ListLoading mobileView={ mobileView} isLoading={allDemoBuckets.loading} buckets={allDemoBuckets.data} />
);
}
However, Im not sure how to clean up this effect above, I've tried mounting it using True and False, however the error still showed up. How can I fix my function above so the useEffect doesnt throw any warnings
EDIT:
code for my reduer:
export const getAllDemoBuckets = (dispatch) => axiosInstance
.get('demo/all/')
.then(response => {
dispatch({ type: 'FETCH_SUCCESS', payload: response.data })
console.log('fired bucket-data')
})
.catch(error => {
dispatch({ type: 'FETCH_ERROR' })
})
const initialStateAllBuckets = {
loading: true,
error: '',
data: []
}
const reducer = (state, action) =>
{
switch (action.type)
{
case 'FETCH_SUCCESS':
return {
loading: false,
data: action.payload,
error: ''
}
case 'FETCH_ERROR':
return {
loading: false,
data: {},
error: "Something went wrong!"
}
default:
return state
}
}
const [allDemoBuckets, dispatchAllBuckets] = useReducer(reducer, initialStateAllBuckets)
The goal of the warning is to tell you that some action is taking place after the component is unmounted and that the result of that work is going to be thrown away.
The solution isn't to try and work around it with a reducer; the solution is to cancel whatever is happening by returning a callback from useEffect. For example:
useEffect(() => {
const ctrl = new AbortController();
fetchExternalResource(ctrl.signal);
return () => {
ctrl.abort();
}
}, []);
Using flags to determine if a component is mounted (ie using a reducer) to determine whether or not to update state is missing the point of the warning.
It's also okay to leave the warning up if this isn't actually an issue. It's just there to nit pick and tell you that, hey, you may want to clean this up. But it's not an error.
In your case, if you are using fetch, I would modify your code such that the function that dispatches actions can take an AbortSignal to cancel its operations. If you're not using fetch, there's not much you can do, and you should just ignore this warning. It's not a big deal.
It looks like you're using Axios for your requests. Axios supports a mechanism similar to abort signals - This should do the trick.
import { CancelToken } from 'axios';
const getAllDemoBuckets = async (dispatch, cancelToken) => {
try {
const response = await axiosInstance.get('/demo/all', { cancelToken });
dispatch({ type: 'FETCH_SUCCESS', payload: response.data });
} catch (err) {
if ('isCancel' in err && err.isCancel()) {
return;
}
dispatch({ type: 'FETCH_ERROR' });
}
}
const MyComponent = () => {
useEffect(() => {
const source = CancelToken.source();
getAllDemoBuckets(dispatch, source.token);
return () => {
source.cancel();
};
}, []);
}

Why list disappeared when I remake small part React app to Redux?

I have app wrote on pure React where I make request to server and get response - category list. But I needed to rework small part of my app.
But when I remake this part my list dissapeared.
I tried write console.log("My data look like:", data); after const data = await api('pathWithQueryParams', {. to see if the data is coming. But I don't even see the text My data look like: in browser console. That is, in inside the function fetchData even console.log dont work.
First in my question I'll write code that I remake to Redux
and below after _______________________________
I'll write small part my app which wrote on pure React(before remake to redux) and work well.
Wrote on REDUX:
Home.js:
const Home = () => {
const listCategory = useSelector(state => state.filterListReducer.listCategory);
const currentPage = useSelector(state => state.filterListReducer.currentPage);
const quantityElementPage = useSelector(state => state.filterListReducer.quantityElementPage);
const sortAscDesc = useSelector(state => state.filterListReducer.sortAscDesc);
const searchInput = useSelector(state => state.filterListReducer.searchInput);
useEffect(() => {
fetchData(currentPage, quantityElementPage, sortAscDesc, searchInput);
}, [currentPage, quantityElementPage, sortAscDesc, searchInput]);
async function fetchData(valuePage, valueElement, valueSort, valueFilter, dispatch ) {
return async (dispatch) => {
try {
dispatch({ type: "LOAD_DATA_START" });
const data = await api(`pathWithQueryParams`, { // <-- api - it function which using fetch make request to server
method: 'GET',
});
console.log("My data look like:", data); // <-- check if the data came in response, but I don't see even text "My data look like:" in browser console
dispatch({ type: "LOAD_DATA_END", payload: data });
} catch (e) {
console.error(e);
}
};
}
return ( <div> <Table dataAttribute={listCategory} /> </div> ); };
___________________________________________________
Wrote on pure React (before remake to redux):
const Home = () => {
const [value, setValue] = useState({
listCategory: [],
currentPage: 1,
quantityElementPage: 3,
buttonsPagination: 0,
buttonsQuantityElementPage: 3,
sortAscDesc: "asc",
searchInput: ""
});
useEffect(() => {
fetchData(value.currentPage, value.quantityElementPage, value.sortAscDesc, value.searchInput);
}, [value.currentPage, value.quantityElementPage, value.sortAscDesc, value.searchInput]);
async function fetchData(valuePage, valueElement, valueSort, valueFilter ) {
try {
const data = await api(`pathWithQueryParams`, {
method: 'GET',
});
setValue(prev => ({
...prev,
listCategory: data.data,
currentPage: data.page,
buttonsPagination: Math.ceil(data.total / data.perPage),
quantityElementPage: data.perPage,
}));
} catch (e) {
console.error(e);
}
}
// Home.js fragment
useEffect(() => {
/*
fetchData(currentPage, quantityElementPage, sortAscDesc, searchInput);
invoke fetchData, but does not call returning function
e.g. const actualFetching = fetchData(currentPage, quantityElementPage, sortAscDesc, searchInput);
and missing call actualFetching(dispatch);
*/
fetchData(currentPage, quantityElementPage, sortAscDesc, searchInput)(dispatch);
}, [currentPage, quantityElementPage, sortAscDesc, searchInput]);
async function fetchData(valuePage, valueElement, valueSort, valueFilter
/*, dispatch - not necessary here */ ) {
/*
returning function (dispatch) => {}
*/
return async (dispatch) => {
try {
dispatch({ type: "LOAD_DATA_START" });
const data = await api(`pathWithQueryParams`, { // <-- api - it function which using fetch make request to server
method: 'GET',
});
console.log("My data look like:", data); // <-- check if the data came in response, but I don't see even text "My data look like:" in browser console
dispatch({ type: "LOAD_DATA_END", payload: data });
} catch (e) {
console.error(e);
}
};
}
async function fetchData(valuePage, valueElement, valueSort, valueFilter, dispatch ) {
return async (dispatch) => {
try {
dispatch({ type: "LOAD_DATA_START" });
const data = await api(`pathWithQueryParams`, { // <-- api - it function which using fetch make request to server
method: 'GET',
});
console.log("My data look like:", data); // <-- check if the data came in response, but I don't see even text "My data look like:" in browser console
dispatch({ type: "LOAD_DATA_END", payload: data });
} catch (e) {
console.error(e);
}
};
}
This function is now returning a function, I think you are trying to create a hook here so the way to probably do this is:
useEffect(() => {
const fetch = fetchData();//this returns a function now
fetch(currentPage, quantityElementPage, sortAscDesc, searchInput);
}, [currentPage, quantityElementPage, sortAscDesc, searchInput]);
function fetchData() {
return async (valuePage, valueElement, valueSort, valueFilter, dispatch) => {
try {
dispatch({ type: "LOAD_DATA_START" });
const data = await api(`pathWithQueryParams`, {
// <-- api - it function which using fetch make request to server
method: "GET"
});
console.log("My data look like:", data); // <-- check if the data came in response, but I don't see even text "My data look like:" in browser console
dispatch({ type: "LOAD_DATA_END", payload: data });
} catch (e) {
console.error(e);
}
};
}

Why is State always empty?

I have thoroughly gone through all the asked question and none of them apply to my problem directly. I am looping through an array of user ids and matching them to get a user from my firestore db. I get the result back with no problem but when i store it in the state array and run a console log, my state array is always empty. The first console.log works and shows the results from the db.
Here's my code:
const UsersScreen = (props) => {
const [state, setState] = useState({
users: []
});
const getUserProfiles = () => {
let users = [];
//networkUsers is an array with the ids
networkUsers.forEach(userId => {
db.doc(userId).get().then((doc) => {
users.push(doc.data());
console.log('localusers', users)
}).catch((error) => {
console.log('caught error', error)
})
});
setState({ users: users });
};
useEffect(() => {
getUserProfiles();
}, []);
console.log('state', state.users)
}
Please help.
The logic that fetches the document from Firestore is asynchronous. The call to setState is synchronous though. It will always before the document has been fetched. The solution would be to fetch the documents then set the state. Here is an example:
const UsersScreen = (props) => {
const [state, setState] = useState({
users: [],
});
const getUserProfiles = () => {
Promise.all(networkUsers.map((userID) => db.doc(userId).get()))
.then((docs) => {
setState({ users: docs.map((doc) => doc.data()) });
})
.catch((err) => {
console.log("caught error", error);
});
};
useEffect(() => {
getUserProfiles();
}, []);
console.log("state", state.users);
};
The Promise.all call resolves once every user has been fetched from the Firestore (maybe you could fetch them at once though). Once we have the users we loop over them with map to extract the data of the document and set the state. Here is an alternative with async/await:
const UsersScreen = (props) => {
const [state, setState] = useState({
users: [],
});
const getUserProfiles = async () => {
try {
const docs = await Promise.all(
networkUsers.map((userID) => db.doc(userId).get())
);
setState({ users: docs.map((doc) => doc.data()) });
} catch (err) {
console.log("caught error", error);
}
};
useEffect(() => {
getUserProfiles();
}, []);
console.log("state", state.users);
};

How can I pass a param to a promise inside usePromise React hook

I created an usePromise React Hook that should be able to resolve every kind of javascript promise and returns every result and state: data, resolving state and error.
Im able to make it work passing the function without any param, but when I try to change it to allow a param, I get an infinite loop.
const usePromise = (promise: any): [any, boolean, any] => {
const [data, setData] = useState<object | null>(null);
const [error, setError] = useState<object | null>(null);
const [fetching, setFetchingState] = useState<boolean>(true);
useEffect(() => {
setFetchingState(true);
promise
.then((data: object) => {
setData(data);
})
.catch((error: object) => {
setError(error);
})
.finally(() => {
setFetchingState(false);
});
}, [promise]);
return [data, fetching, error];
};
const apiCall = (param?: string): Promise<any> => {
return new Promise(resolve => {
setTimeout(() => {
resolve({ response: `Response generated with your param ${param}.` });
}, 500);
});
};
const App = (): React.Element => {
// How can I pass an argument to apiCall?
const [response, fetching, error] = usePromise(apiCall(5));
console.log("render"); // This logs infinitely
return <div>{JSON.stringify({ response, fetching, error })}</div>;
};
You can check the working code (without params) at: https://codesandbox.io/s/react-typescript-fl13w
And the bug at (The tab gets stuck, be adviced): https://codesandbox.io/s/react-typescript-9ow82
Note: I would like to find the solution without using a usePromise single function library from NPM or similar
Custom hooks might be executed multiple times. You should design it that way, that everything you want to do just once (e.g. the API call) is inside a useEffect hook. That can be achieved by taking a callback that gets then called in a hook.
Also, slightly more typesafe:
const usePromise = <T>(task: () => Promise<T>) => {
const [state, setState] = useState<[T?, boolean, Error?]>([null, true, null]);
useEffect(() => {
task()
.then(result => setState([result, false, null])
.catch(error => setState([null, false, error]);
}, []); // << omit the condition here, functions don't equal each other²
return state;
};
// Then used as
usePromise(() => apiCall(5));
² yes, thats generally a bad practice, but as task is not supposed to change here, I think that's fine
Upon request, here's a version that I use in some of my projects:
export function useAPI<Q, R>(api: (query: Q) => Promise<R | never>) {
const [state, setState] = useState<{ loading?: true, pending?: true, error?: string, errorCode?: number, result?: R }>({ pending: true });
async function run(query: Q) {
if(state.loading) return;
setState({ loading: true });
try {
const result = await api(query);
setState({ result });
} catch(error) {
if(error instanceof HTTPError) {
console.error(`API Error: ${error.path}`, error);
setState({ error: error.message, errorCode: error.code });
} else {
setState({ error: error.message, errorCode: NaN });
}
}
}
function reset() {
setState({ pending: true });
}
return [state, run, reset] as const;
}

Categories