I want stop rendering in react-hooks - reactjs

It's continuing to be rendered. I want to stop it.
Because of the problem, photos and text received from Api keep changing randomly.
I think useEffect is the problem. Please let me know because I am a beginner.
This is useFetch.jsx
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
useEffect(() => {
fetchUrl();
});
return [data, loading];
}
export default useFetch;
This is Main.jsx
import React from "react";
import styled from "styled-components";
import useFetch from "./useFetch";
import "./font.css";
const url = "https://www.thecocktaildb.com/api/json/v1/1/random.php";
const Main = () => {
const [data, loading] = useFetch(url);
return (
<Wrapper>
<Header>My Cocktail Recipe</Header>
{loading ? (
"Loading..."
) : (
<>
{data.drinks.map(
({ idDrink, strDrink, strAlcoholic, strGlass, strDrinkThumb }) => (
<Container>
<img src={`${strDrinkThumb}`} alt="" />
<div key={`${idDrink}`}>{`${strDrink}`}</div>
</Container>
)
)}
</>
)}
<Search type="text" placeholder="검색하세요" val />
</Wrapper>
);
};
export default Main;

You must update useFetch.jsx:
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
useEffect(() => {
fetchUrl();
}, []); //<--- Here
return [data, loading];
}
export default useFetch;
The problem is that the useEffect hook receives two arguments and you forgot the second argument, which is the effect's dependencies array.

useEffect(() => {
fetchUrl();
}, []);
return [data, loading];
}
Add Array Thanks Nick Parsons

Related

React. How to show data loading message on API call

Trying to render an API call which returns an array of products.
How to show loading message on products render. (Currently the "loading" message is not being displayed)
useGetProducts.js
import { useEffect, useState } from "react";
import axios from "axios";
const useGetProducts = (API) => {
const [products, setProducts] = useState(null);
const [error, setError] = useState("");
const [loaded, setLoaded] = useState(false);
useEffect(() => {
(async () => {
try {
async function fetchData() {
const response = await axios(API);
setProducts(response.data)
}
fetchData();
} catch (error) {
setError(error.message);
} finally {
setLoaded(true);
}
})();
}, []);
return { products, error, loaded };
};
export default useGetProducts
ProductList.js
import React from "react";
import ProductItem from "./ProductItem";
import useGetProducts from "../hooks/useGetProducts";
import { Link } from "react-router-dom";
import { API } from '../constants/constants';
const ProductList = () => {
const data = useGetProducts(`${API}?limit=9&offset=0`);
return (
<section className="theme-section">
{data.loaded ?
<>
{data.products && data.products.map((product) => (
<div key={product.id}>
<ProductItem product={product} />
<Link to={`/product/${product.id}`}>ver detalle</Link>
<br /><br />
<hr />
</div>
))}
</>
: "loading"
}
</section>
)
}
export default ProductList;
Currently the message is not being displayed
What is happening here is that you're calling fetchData without waiting for it, which is immediately setting loaded to true.
I don't think there is a need for the fetchData function here, so either remove it or await it:
const [products, setProducts] = useState(null);
const [error, setError] = useState("");
const [loaded, setLoaded] = useState(false);
useEffect(() => {
(async () => {
const response = await axios(API);
setProducts(response.data)
} catch (error) {
setError(error.message);
} finally {
setLoaded(true);
}
})();
}, []);
set your loaded state to false when you enter in fetchData function and in finally method, update loaded state to true.
useEffect(() => {
(async () => {
try {
async function fetchData() {
setLoaded(false);
const response = await axios(API);
setProducts(response.data);
}
fetchData();
} catch (error) {
setError(error.message);
} finally {
setLoaded(true);
}
})();
}, []);

Stop Rendering before data load using hooks

I am new to react hooks I write a react custom hook
Hook:
import { useState, useEffect } from 'react';
export const useFetch = (url, options) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
} catch (error) {
setError(error);
}
};
fetchData();
}, []);
return { response, error };
};
And I also write a functional component and i want component render when data comes
here is my component
Component
import React, { useState, useEffect } from 'react';
import './index.scss';
import { List } from '../components';
import { useFetch } from '../../hooks';
export const Subscription = () => {
const res = useFetch('http://localhost:8080/test', {});
const [isLoading, setLoading] = useState(true);
useEffect(() => {
if (res.response.length > 0) {
console.log('this is the test');
setLoading(false);
}
});
const list = res.response;
return (
<div>
{isLoading && <div>Loading...</div>}
{!isLoading && (
<div className="list">
<List subscriptions={list} />
</div>
)}
</div>
);
};
but i am unable to render List component I didn't understand once data comes from backend why list note having data still it having null value and lists is not renderd
I got proper values from backend
useFetch return return { response, error }; ==> const response = useFetch('http://localhost:8080/test', {}); the response is an object containing { response, error }
Do this instead const {response} = useFetch('http://localhost:8080/test', {});
And you should handle loading in useFetch
UseFetch
import { useState, useEffect } from 'react';
export const useFetch = (url, options) => {
const [response, setResponse] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
setLoading(false)
} catch (error) {
setError(error);
setLoading(false)
}
};
fetchData();
}, []);
return { response, error,loading };
};
Subscription
import React, { useState, useEffect } from 'react';
import './index.scss';
import { List } from '../components';
mport { useFetch } from '../../hooks';
export const Subscription = () => {
const {response: subscriptions, loading} = useFetch('http://localhost:8080/test', {});
return (
<div>
{isLoading && <div>Loading...</div>}
{!isLoading && (
<div className="list">
<List subscriptions={subscriptions} />
</div>
)}
</div>
);
};

How to pass state data from custom hooks to react component?

I have custom hook(useFetch) that takes URL as input and fetch data from that URL and returns data. I want to implement spinner (already made Spinner component ) on my other components and I tried by making state for the isLoading and setIsLoading of spinner.
my custom hook code:
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return dataArray;
};
export default useFetch;
This is the component that I want to implement spinner.
import React, { useState } from 'react';
import CONSTANTS from '../../constants/constants';
import CompanyLists from '../../components/company-lists/CompanyLists';
import Pagination from '../../components/pagination/Pagination';
import useFetch from '../../components/effects/use-fetch.effect';
import Spinner from '../../components/spinner/Spinner';
const CompanyListing = () => {
const [counter, setCounter] = useState(1);
const companies = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
return (
<>
<Container>
<div style={userStyle}>
{companies ? companies.map((company) => <CompanyLists key={company.id} {...company} />) : 'No companies'}
</div>
<Pagination props={companies} counter={counter} name="companies" setCounter={setCounter} />
<Spinner />
</Container>
</>
);
};
const userStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(1, 1fr)',
gridGap: '1rem',
};
export default CompanyListing;
Problem here is: How can I send those loading state from hook to CompanyListing component. Any help will be appreciated.
EDIT:
I have other component that also calls same hook and I want them not to be broken. As I didn't mention on original question .
My another case:
const jobsUrl = `${CONSTANTS.BASE_URL}/jobs?page=${counter}`;
const jobs = useFetch(jobsUrl);
AND
const { city, company_name, company_id, department, description, job_type, position, posted_at, url } = useFetch(
`${CONSTANTS.BASE_URL}/jobs/${id}`
);
How can I destructure in these two cases ?
You do not need anything special here. Just return the isLoading state with the dataArray from the useFetch hook.
As mentioned from the edit you need the useFetch to be more reusable and return data in different formats depending on the API response, hence the state should be initialized as null.
import { useState, useEffect } from 'react';
const useFetch = (url) => {
// it is best to initialize the state as null because response.data
// may be an object or an array depending on the API response
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const value = await res.json();
setData(value.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return {data, isLoading};
};
export default useFetch;
In the components you want to use the custom hook, you can destructure
the value for data and isLoading but to futher destructure values from the returned data we have to check if data is null
// destructure the values
const {data, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
// in this case data will be an array based on your API response
// please make sure to check data.length before trying to loop over
// and render the content, for example
return (
<div>
{
data.length && data.map(company => (
<CompanyLists key={company.id} {...company} />
));
}
</div>
)
For the second case where you will be fetching data using useFetch(`${CONSTANTS.BASE_URL}/jobs/${id}`); you have to check if the returned data is not null before destructuring further. Example
const { data, isLoading } = useFetch(`${CONSTANTS.BASE_URL}/jobs/${id});
if (isLoading) {
return <div>Loading...</div>
}
if (data) {
const {
city,
company_name,
company_id,
department,
description,
job_type, position, posted_at, url } = data;
return (
// your jsx code
// for example
<h3>{company_name}</h3>
<p>{department}</p>
)
}
You need to return isLoading as well from the hook.
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
fetchData();
} catch (err) {
console.error(err);
}
setIsLoading(false);
}, [url]);
return {dataArray, isLoading};
};
export default useFetch;
And use this in your component like this
import React, { useState } from 'react';
import CONSTANTS from '../../constants/constants';
import CompanyLists from '../../components/company-lists/CompanyLists';
import Pagination from '../../components/pagination/Pagination';
import useFetch from '../../components/effects/use-fetch.effect';
import Spinner from '../../components/spinner/Spinner';
const CompanyListing = () => {
const [counter, setCounter] = useState(1);
const {dataArray: companies, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
return (
<>
<Container>
<div style={userStyle}>
{companies ? companies.map((company) => <CompanyLists key={company.id} {...company} />) : 'No companies'}
</div>
<Pagination props={companies} counter={counter} name="companies" setCounter={setCounter} />
{isLoading && <Spinner />}
</Container>
</>
);
};
const userStyle = {
display: 'grid',
gridTemplateColumns: 'repeat(1, 1fr)',
gridGap: '1rem',
};
export default CompanyListing;
From your custom hook you can return both like
return {companies: dataArray, isLoading };
And destruct both
const {companies, isLoading} = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);
First, you might need to change your useFetch effect a bit to update isLoading correctly. Then you could return both dataArray and isLoading :
import { useState, useEffect } from 'react';
const useFetch = (url) => {
const [dataArray, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
try {
const fetchData = async () => {
setIsLoading(true);
const res = await fetch(url);
const dataArray = await res.json();
setData(dataArray.data);
};
await fetchData();
} catch (err) {
console.error(err);
} finally {
setIsLoading(false);
}
}, [url]);
return [dataArray, isLoading];
};
export default useFetch;
And use it like the following :
const [companies, isLoading] = useFetch(`${CONSTANTS.BASE_URL}/companies?page=${counter}`);

Using debounce with React hooks causes endless requests

I need to get data from server on changes in search input but I don't want to send a new request on every new character there so I'm trying use debounce from use-debounce package https://github.com/xnimorz/use-debounce. But my code below causes only endless requests before even any changes in search input happens.
App.js
import React, { useState, useEffect } from "react";
import axios from "axios";
import moment from "moment";
import { useDebounce } from "use-debounce";
import { Layout } from "./../Layout";
import { List } from "./../List";
import { Loader } from "./../Loader";
import { Header } from "./../Header";
import { Search } from "./../Search";
import { Licenses } from "./../Licenses";
import { Pagination } from "./../Pagination";
import "./App.css";
const PER_PAGE = 20;
export const App = () => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [hasError, setHasError] = useState(false);
const [nameSearch, setNameSearch] = useState("");
const [license, setLicense] = useState({});
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const debouncedNameSearch = useDebounce(nameSearch, 2000);
const fetchData = async () => {
setHasError(false);
setIsLoading(true);
try {
const prevMonth = moment()
.subtract(30, "days")
.format("YYYY-MM-DD");
const licenseKey = (license && license.key) || "";
const url = `https://api.github.com/search/repositories?q=${nameSearch}+in:name+language:javascript+created:${prevMonth}${
licenseKey ? `+license:${licenseKey}` : ""
}&sort=stars&order=desc&page=${currentPage}&per_page=${PER_PAGE}`;
const response = await axios(url);
setData(response.data.items);
setTotal(response.data.total_count);
} catch (error) {
setHasError(true);
setData([]);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, [license, nameSearch, currentPage]);
return (
<Layout>
<Header>
<Search
handleNameSearchChange={setNameSearch}
nameSearch={nameSearch}
/>
<Licenses license={license} handleLicenseChange={setLicense} />
</Header>
<main>
{hasError && <div>Error...</div>}
{isLoading && <Loader />}
{data && !isLoading && !hasError && (
<>
<List data={data} />
<Pagination
currentPage={currentPage}
total={total}
itemsPerPage={PER_PAGE}
handlePageChange={setCurrentPage}
/>
</>
)}
</main>
</Layout>
);
};
Search.js
import React from "react";
import PropTypes from "prop-types";
export const Search = ({ handleNameSearchChange, nameSearch }) => (
<div className="flex-grow-1 mx-lg-3 mb-4 mb-lg-0">
<input
type="text"
name="search"
placeholder="Enter name..."
onChange={e => handleNameSearchChange(e.target.value)}
className="form-control"
value={nameSearch}
/>
</div>
);
Search.propTypes = {
nameSearch: PropTypes.string,
handleNameSearchChange: PropTypes.func
};
How to make debounce work properly?
You never refer to debouncedNameSearch.
I think the issue is with your useEffect:
useEffect(() => {
fetchData();
}, [license, nameSearch, currentPage]);
The first issue is that it will fire every time nameSearch changes, so you should change it to use debouncedNameSearch:
useEffect(() => {
fetchData();
}, [license, debouncedNameSearch, currentPage]);
You are also firing the request on initial render when debouncedNameSearch is an empty string, so you could wrap the call to fetchData in a conditional to prevent the request firing when debouncedNameSearch === "":
useEffect(() => {
if(debouncedNameSearch) {
fetchData();
}
}, [license, debouncedNameSearch, currentPage]);
Also, your request is using nameSearch when it should be using debouncedNameSearch:
const url = `https://api.github.com/search/repositories?q=${nameSearch}...
Change to:
const url = `https://api.github.com/search/repositories?q=${debouncedNameSearch}...
And it's recommended that any function that is declared inside a component and called inside a useEffect should either be declared inside the useEffect, or set as a dependency of that useEffect:
Read the docs: is it safe to omit functions from the list of dependencies?
So you can either do something like this:
useEffect(() => {
// Declare fetchData inside useEffect
const fetchData = async () => {...};
if (debouncedNameSearch) {
// Call it inside useEffect too
fetchData();
}
}, [
// Don't forget to add the function's dependencies
license,
debouncedNameSearch,
currentPage,
setHasError,
setIsLoading,
setData,
setTotal
]);
Or you can make the function itself a dependency of the useEffect, but you should wrap the function in a useCallback to make sure its state dependencies are up to date (as per the documentation linked above):
const fetchData = useCallback(
async () => {
// Function defined here
},
[ // function dependencies
setHasError,
setIsLoading,
license,
debouncedNameSearch,
currentPage,
setData,
setTotal,
setHasError
]
);
useEffect(() => {
if(debouncedNameSearch) {
fetchData();
}
}, [license, debouncedNameSearch, currentPage, fetchData]); // Add as dependency

How to refetch data when field change with react hooks

My component fetches data by calling an hook-file which contains logic for requesting via API.
By default it will call the API without any extra parameter.
In GUI I also show an input where use can enter text.
Each time he writes a letter I want to refetch data. But Im not really sure how to do this with react and hooks.
I declared "useEffect". And I see that the content of the input changes. But what more? I cannot call the hook-function from there because I then get this error:
"React Hook "useFetch" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks"
This is the code:
hooks.js
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
fetchUrl();
}, [url]);
return [data, loading];
}
export { useFetch };
mycomponent.js
import React, { useState, useEffect } from 'react';
import { useFetch } from "../hooks";
const MyComponent = () => {
useEffect(() => {
console.log('rendered!');
console.log('searchTerm!',searchTerm);
});
const [searchTerm, setSearchTerm] = useState('');
const [data, loading] = useFetch(
"http://localhost:8000/endpoint?${searchTerm}"
);
return (
<>
<h1>Users</h1>
<p>
<input type="text" placeholder="Search" id="searchQuery" onChange={(e) => setSearchTerm(e.target.value)} />
</p>
{loading ? (
"Loading..."
) : (
<div>
{data.users.map((obj) => (
<div key={`${obj.id}`}>
{`${obj.firstName}`} {`${obj.lastName}`}
</div>
))}
</div>
)}
</>
);
}
export default MyComponent;
Create a function to handle your onChange event and call your fetch function from it. Something like this:
mycomponent.js
import React, { useState, useEffect } from 'react';
import { useFetch } from "../hooks";
const MyComponent = () => {
useEffect(() => {
console.log('rendered!');
console.log('searchTerm!',searchTerm);
});
const [searchTerm, setSearchTerm] = useState('');
const handleChange = e => {
setSearchTerm(e.target.value)
useFetch(
"http://localhost:8000/endpoint?${searchTerm}"
);
}
const [data, loading] = useFetch(
"http://localhost:8000/endpoint?${searchTerm}"
);
return (
<>
<h1>Users</h1>
<p>
<input type="text" placeholder="Search" id="searchQuery" onChange={(e) => handleChange(e)} />
</p>
{loading ? (
"Loading..."
) : (
<div>
{data.users.map((obj) => (
<div key={`${obj.id}`}>
{`${obj.firstName}`} {`${obj.lastName}`}
</div>
))}
</div>
)}
</>
);
}
export default MyComponent;
Your code works for me as per your requirement, type 1 or 2 in text box you will have different results.
So basically API get called once with default value of "searchTerm" and then it get called for each time by onChange.
try this at your local -
import React, { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
setLoading(false);
}
fetchUrl();
}, [url]);
return [data, loading];
}
export { useFetch };
const MyComponent = () => {
useEffect(() => {
console.log("rendered!");
console.log("searchTerm!", searchTerm);
});
const [searchTerm, setSearchTerm] = useState("");
const [data, loading] = useFetch(
`https://reqres.in/api/users?page=${searchTerm}`
);
return (
<>
<h1>Users</h1>
<p>
<input
type="text"
placeholder="Search"
id="searchQuery"
onChange={e => setSearchTerm(e.target.value)}
/>
</p>
{loading ? (
"Loading..."
) : (
<div>
{data.data.map(obj => (
<div key={`${obj.id}`}>
{`${obj.first_name}`} {`${obj.last_name}`}
</div>
))}
</div>
)}
</>
);
};
export default MyComponent;
The way your useFetch hook is setup it will only run once on load. You need to have it setup in a way you can trigger it from an effect function that runs only when searchTerm changes.
this is how you handle searching in react properly. It is better to have default searchTerm defined when user lands on your page, because otherwise they will see empty page or seening "loading" text which is not a good user experience.
const [data, setData] = useState([]);
const [searchTerm, setSearchTerm] = useState("defaultTerm")
In the first render of page, we should be showing the results of "defaultTerm" search to the user. However, if you do not set up a guard, in each keystroke, your app is going to make api requests which will slow down your app.
To avoid fetching data in each keystroke, we set up "setTimeout" for maybe 500 ms. then each time user types in different search term we have to make sure we clean up previous setTimeout function, so our app will not have memory leak.
useEffect(() => {
async function fetchUrl() {
const response = await fetch(url);
const json = await response.json();
setData(json);
}
// this is during initial rendering. we have default term but no data yet
if(searchTerm && !data){
fetchUrl();
}else{
//setTimeout returns an id
const timerId=setTimeout(()=>{
if(searchTerm){
fetchUrl}
},500)
// this where we do clean up
return ()=>{clearTimeout(timerId)}
}
}, [url]);
return [data, loading];
}
inside useEffect we are allowed to return only a function which is responsible for cleaning up. So right before we call useEffect again, we stop the last setTimeout.

Resources