React: How optimize a custom hook that shares data? - javascript

I have a Custom Hook similarly with the below:
import { useEffect, useState } from 'react';
import axios from 'axios';
const myCustomHook = () => {
const [countries, setCountries] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
(async () =>
await axios
.get("MY_API/countries")
.then(response => setCountries(response.data))
.finally(() => setLoading(false)))();
}, []);
return countries;
};
export default myCustomHook;
The hook works great but I am using it in three different areas of my application despite the fact that all the countries are the same wherever the hook is used.
Is there a good pattern to call the axios request just once instead of three times?
EDIT - Final Code After Solution
import { useEffect, useState } from 'react';
import axios from 'axios';
let fakeCache = {
alreadyCalled: false,
countries: []
};
const myCustomHook = (forceUpdate = false) => {
const [isLoading, setLoading] = useState(true);
if (!fakeCache.alreadyCalled || forceUpdate) {
fakeCache.alreadyCalled = true;
(async () =>
await axios
.get("MY_API/countries")
.then(response => setCountries(response.data))
.finally(() => setLoading(false)))();
}
return countries;
};
export default myCustomHook;

One solution to this would be to introduce a custom "cacheing layer" (between your hook and the axios request) that:
caches countries data returned from the first successful request and,
return the same cached data on subsequent requests
There are a number of ways this could be implemented - one possibility would be to define a getCountries() function that implements that cacheing logic in a separate module, and then call that function from your hook:
countries.js
import axios from 'axios';
// Module scoped variable that holds cache data
let cachedData = undefined;
// Example function wraps network request with cacheing layer
export const getCountries = async() => {
// We expect the data for countries to be an array. If cachedData
// is not an array, attempts to populate the cache with data
if (!Array.isArray(cachedData)) {
const response = await axios.get("MY_API/countries");
// Populate the cache with data returned from request
cachedData = response.data;
}
return cachedData;
}
myCustomHook.js
import { useEffect, useState } from 'react';
import { getCountries } from "/countries";
const myCustomHook = () => {
const [countries, setCountries] = useState([]);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
(async() => {
try {
setLoading(true);
// Update hook state with countries data (cached or fresh)
setCountries(await getCountries());
} finally {
setLoading(false)
}
}, []);
});
}
export default myCustomHook;

Each instance of the component runs independently of each other.
Even though the 1st instance state has countries populated the 2nd instance is not aware of it and is still empty.
You could instead consume countries as a prop and if empty invoke the effect, otherwise just return the countries from props.

Related

What is the exact functionality of Search (Data.Search) function in the Code is it built-in funnction to search in Json?

what is Data.search here
import React, { useState, useEffect } from "react";
import MovieCard from "./MovieCard";
import SearchIcon from "./search.svg";
import "./App.css";
const API_URL = "http://www.omdbapi.com?apikey=b6003d8a";
const App = () => {
const [searchTerm, setSearchTerm] = useState("");
const [movies, setMovies] = useState([]);
useEffect(() => {
searchMovies("Batman");
}, []);
const searchMovies = async (title) => {
const response = await fetch(`${API_URL}&s=${title}`);
const data = await response.json();
setMovies(data.Search);
};
why don't we write simply setMovies(data);
no, it is not. you are fetching data from an api and it returns an object, which has a value called search, therefore you are setting data.search value as your movies, not entire object returned from the api, you can check this easily by logging both fields, like so :
const data = await response.json();
console.log('this is data' , data)
console.log('this is data.search', data.search)
setMovies(data.Search);

Custom API data fetching hook keeps rerendering

I've created a custom hook which I want to use to fetch data with. Now I've come a long way with some help from several blog articles, but there's just one thing I want to improve on. I have a custom hook which fetches data using a useEffect hook. This way the data is fetched upon render, and when for example query params change. Now the useEffect has a caveat. When I include a dependency array with anything in it, it's all fine, but I get a warning that the hook is dependent on a value. I don't like warnings so I add the value to the dependency array, but for some reason then it just keeps rerendering. Below is my useApi hook:
import { useState, useEffect } from "react";
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import api from "./config/api-config";
const useApi = (axiosParams: AxiosRequestConfig) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(axiosParams.method === "GET");
const fetchData = async () => {
try {
const result = await api.request(axiosParams);
setResponse(result);
} catch (err: any) {
setError(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
console.log("Render useApi");
axiosParams.method === "GET" && fetchData();
}, [axiosParams.method, fetchData]);
return { response, error, loading, fetchData };
};
export default useApi;
And this is where I'm using it:
import { FC, useState } from "react";
import { Wrapper } from "./home.style";
import { HomeProps } from "./home.types";
import useApi from "../../../api/useApi";
const Home: FC<HomeProps> = () => {
const [query, setQuery] = useState<String>();
const { response, loading, error, fetchData } = useApi({
method: "GET",
url: "/books/v1/volumes",
params: {
q: "",
},
});
return <Wrapper></Wrapper>;
};
export default Home;
I've tried using a callback hook for the fetchData function, but then the issue with the dependency array moves from the useEffect to the useCallback. Does anyone know how I should handle this?
To answer as an answer:
Infinite reexecution of useApi hook is happening due to the object passed as a parameter is recreated on each component rerendering.
const { ... } = useApi({
method: "GET",
url: "/books/v1/volumes",
params: {
q: "",
},
});
It will work fine in case a parameter is a plain string or a number. But in case of normal object or array, for example, you need to preserve a reference to them. You can either move this config out of functional component scope, i.e. just place it above it (if it is not meant to be modified), either preserve it with useMemo or useState hook.
// const useApiConfig = useMemo<AxiosRequestConfig>(() => {
const useApiConfig = useMemo(() => {
return {
method: "GET",
url: "/books/v1/volumes",
params: { q: "" }
};
}, [])
/* ... */
const { response, loading, error, fetchData } = useApi(useApiConfig);
Additionaly, due to fetchData is in depsArray of useEffect hook - it is important to wrap fetchData into useCallback due to without it fetchData will be recreated on each rerender and useEffect will be triggered. And due to reexport of the fetchData from the hook - component that is using it will also have an issues with rerender.
Usually (when reexport is not needed) method like fetchData is just placed inside of the useEffech hook itself (as a const function).
const useApi = (axiosParams: AxiosRequestConfig) => {
const [response, setResponse] = useState<AxiosResponse>();
const [error, setError] = useState<AxiosError>();
const [loading, setLoading] = useState(axiosParams.method === "GET");
const fetchData = useCallback(async () => {
try {
const result = await api.request(axiosParams);
setResponse(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}, [axiosParams]);
useEffect(() => {
console.log("Render useApi");
axiosParams.method === "GET" && fetchData();
}, [axiosParams.method, fetchData]);
return { response, error, loading, fetchData };
};

Can't get image to load after connecting to an API using react.js

I've created a custom fetch component, and I'm simply trying to get an image to load on the page from an API called "the dog API". Have I missed something crucial?
App.js
import './App.css';
import './Dog.js';
import useFetch from './useFetch';
function DogApp() {
const API_KEY = "";
const { data, loading, error } = useFetch(`https://api.thedogapi.com/v1/images/search/API_KEY=${API_KEY}`);
if (loading) return <h1>Loading the dogs!</h1>
if (error)console.log(error);
return (
<div className="DogApp">
<img src={data?.url}></img>
</div>
);
}
export default DogApp;
UseFetch.js (hook for fetching the data)
import { useEffect, useState } from 'react';
import axios from "axios";
function useFetch(url) {
const [data, setData] = useState(null); //initialize as null depending on what data is
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
axios //make request, if successful it sets data, if not, seterror state
.get(url)
.then((response) => {
setData(response.data);
}).catch((err) => {
setError(err)
}).finally(() => {
setLoading(false);
});
}, [url]);
return {data, loading, error};
}
export default useFetch;
API URL I'm trying to retrieve data from : https://api.thedogapi.com/v1/images/search/
So you're API call (according to the example on thedogapi.com) requires the API key to be set in the header like so:
axios.defaults.headers.common['x-api-key'] = "DEMO-API-KEY"
That fixes the 404, but your code still won't work because the data is returned as an array of objects. So you'll need to map them like so:
{data.map((breed) => (<img src={breed?.url} />))}
I've created a demo sandbox here

Axios request keeps returning twice undefined and twice the data

I'm trying to fetch an api on a custom reactjs hook using Axios. I keep getting twice the response as undefined and after that twice as a successful fetch with the data. The undefined breaks my app.
Btw I'm fetching from the randomuser api.
import axios from "axios";
import { useState, useEffect } from "react"
export const useFetch = (url) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [error, setError] = useState('')
const getData = () => {
setLoading(true)
try {
axios.get(url)
.then(response => setData(response.data));
setLoading(false)
} catch (error) {
setError(error)
}
};
useEffect(() => {
getData()
}, [url])
return {loading, data, error}
}
Trying to use it here and map over it
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useFetch } from '../custom_hooks/useFetch';
const PersonDetails = () => {
const { loading, data , error } = useFetch('https://randomuser.me/api?results=20');
const { results } = data;
const { id } = useParams();
const [person, setPerson] = useState({})
useEffect(() => {
const newPerson = results?.find(person => person.login.uuid === parseInt(id))
setPerson(newPerson)
console.log(newPerson)
}, [])
return (
<div>
{person.name.first}
</div>
)
}
export default PersonDetails
This is the thing I actually Im trying to do, but now because it is undefined, I get that cannot read properties of undefined...
When the effect runs you:
setLoading(true)
Send the Ajax request
setLoading(false)
Later, then the Ajax response arrives you:
setData(response.data)
Since you depend on loading to determine if data is set or not, it breaks.
There are two things you could do:
Move setLoading(false) inside the then callback so it doesn't get set until after you have setData(response.data)
Get rid of loading entirely and base your logic off data being undefined or having a different value.
you should define the getData function inside the useeffect or pass it in dependency array and wrap the function by usecallback to avoid unnecessary rerenders.
you should use abortcontroller in case of cancelations and to have cleanup function in useeffect. (in this case it's better to define getdata body in useeffect)
useEffect(() => {
const controller = new AbortController();
const getData = async () => {
setLoading(true)
try {
await axios.get(url, {signal: controller.signal})
.then(response => setData(response.data));
} catch (error) {
setError(error)
}
}
getData()
return()=>controller.abort()
},[url]}
you can read more about fetching data with hooks in following url and where to setloading and other needed states.
https://www.robinwieruch.de/react-hooks-fetch-data/
Just in case, this solution helped me : https://github.com/axios/axios/issues/2825#issuecomment-883635938
"The problem in my case was caused by React development server.
The strict mode in react caused the issue!
I had to remove the strict mode
This solved the problem of sending double requests!
The strict mode checks are only run in development mode.
Doc: https://reactjs.org/docs/strict-mode.html
"

Not able to use value returned by React hook

I am trying to use a custom hook to make HTTP requests and then use a reducer to update the state in the component.
The hook runs correctly and I can get the response from the request but not able to use the response data in dispatch function.
Below is the code:
HTTP hook:
import React, { Fragment, useState, useEffect, useReducer } from 'react';
import axios from 'axios';
export const useHttpRequest = (initialData, initialURL) => {
const [url, setUrl] = useState(initialURL);
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchData = async () => {
console.log('in a http hook');
setIsError(false);
try {
const res = await axios(url);
console.log(res);
const responseData = res.data.data.data;
return responseData;
} catch (error) {
setIsError(true);
}
};
fetchData();
}, [url]);
return { isError, setUrl };
};
A function call in the state:
const { isError, setUrl } = useHttpRequest();
const getCategoryData = async () => {
setLoading();
try {
const Data = await setUrl('/api/v1/category');
dispatch({
type: SET_CATEGORYDATA,
payload: Data
});
} catch (err) {}
};
A function call in components, where the function is passed through useContext
useEffect(() => {
getCategoryData();
}, []);
You cannot await on a setState (setUrl) function.
You return in your fetch data which is not used later.
You need to first change your mindset on how you think in react hooks and when you need to use them.
As far as I understand you want to fetch some data from server, update the store on successful retrieval, and show an error when the request fails.
You should do this all the way or don't do this at all. You can put the dispatch in the hook or you can forget about the hook and write a reusable fetchData function and handle setHasError in your component's useEffect.
There are many ways to solve this but this is my preferred solution:
import React, { Fragment, useState, useEffect, useReducer } from 'react';
import axios from 'axios';
export const useHttpRequest = (url, updateStore) => {
const [hasError, setHasError] = useState(false);
const fetchData = async (url) => {
setHasError(false);
try {
const res = await axios(url);
const responseData = res.data.data.data;
updateStore(responseData);
} catch (error) {
setHasError(true);
}
};
useEffect(() => {
if (url) {
fetchData(url);
}
}, [url]);
return { fetchData, hasError };
};
// in case you want to fetch the data on component render
const { fetchData, hasError } = useHttpRequest(url, (data) => dispatch({
type: SET_CATEGORYDATA,
payload: data
}));
// in case you want to fetch it in a callback
const clickButton = () => {
fetchData(someCustomUrl);
}
Finally, you can generalize your dispatchers so you don't need to send the whole function to the hook and only send the dispatcher name.

Categories

Resources