Too many re-renders with React Hooks - reactjs

Similar questions have been asked but I haven't found a solution for this particular one. I have one component which renders all boards and I am using a custom useFetch hook to fetch all boards.
const BoardsDashboard = () => {
let [boards, setBoards] = useState([]);
const { response } = useFetch(routes.BOARDS_INDEX_URL, {});
setBoards(response);
return (
<main className="dashboard">
<section className="board-group">
<header>
<div className="board-section-logo">
<span className="person-logo"></span>
</div>
<h2>Personal Boards</h2>
</header>
<ul className="dashboard-board-tiles">
{boards.map(board => (
<BoardTile title={board.title} id={board.id} key={board.id} />
))}
<CreateBoardTile />
</ul>
</section>
</main>
);
};
const useFetch = (url, options) => {
const [response, setResponse] = useState([]);
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 };
};
I am getting too many re-renders due to setBoards(response) line. What is the right way to handle this?
Thanks!

Sounds like you might want a useEffect hook to take action when response is updated.
useEffect(() => {
setBoards(response);
}, [response]);
Note: if you have no need to ever change the boards state, then maybe it doesn’t need to be stateful at all, you could just use the returned value from your useFetch hook and be done with it.
const { response: boards } = useFetch(...);

Related

Render fetched API json object in react component Typescript

i have my json received from api call and is saved in the state "data"
i want to show a loading screen while api is being fetched like i have a state for that too "Loading"
Loading ? Then render data on elements : Loading..
const App = () => {
const [data, setData] = useState([]);
const [Loading, setLoading] = useState(false);
useEffect(() => {
Fetchapi();
}, []);
const Fetchapi = async () => {
try {
await axios.get("http://localhost:8081/api").then((response) => {
const allData = response.data;
setData(allData);
});
setLoading(true);
} catch (e) {
console.log(e);
}
};
return (
<div>
i need my json object rendered here i tried map method on data and i am
getting errors and i have my json2ts interfaces imported in this
</div>
);
};
export default App;
I would camelCase your values/functions and move your fetchApi into the effect itself, as currently its a dependency.
Put setLoading(true) above your fetch request as currently it's not activating until the fetch goes through.
Then below it put setLoading(false), and also inside of your catch.
In your return statement you can now add something like this:
<div>
{loading ? "Loading..." : JSON.stringify(data)}
</div>
Edit
Example for the commented requests.
import { Clan } from "../clan.jsx"
// App
<div>
{loading ? "Loading..." : <Clan data={data}/>}
</div>
// New file clan.jsx
export const Clan = (props) => {
return (
<div>
<h1>{props.data.clan.name}</h1>
</div>
);
}
try this
interface ResponseData {
id: string
// other data ...
}
const App = () => {
const [data, setData] = useState<ResponseData | null>(null)
const [Loading, setLoading] = useState(true)
useEffect(() => {
Fetchapi()
}, [])
const Fetchapi = async () => {
try {
setLoading(true) // USE BEFORE FETCH
await axios.get("http://localhost:8081/api").then(response => {
setLoading(false) // SET LOADING FALSE AFTER GETTING DATA
const allData: ResponseData = response.data
setData(allData)
})
} catch (e) {
setLoading(false) // ALSO SET LOADING FALSE IF ERROR
console.log(e)
}
}
if (Loading) return <p>Loading...</p>
if (data?.length)
return (
<div>
{data.map(d => (
<div key={d.id}>{d.id}</div>
))}
</div>
)
return <div>no data found</div>
}
export default App

Use multiple React useState() hooks to fetch Category title and posts from json api

I am using React useState() and useEffect to fetch multiple data from an api (mock-data/my-data.json).
From the api I want to fetch the Category Title and the list of Posts and render those in the jsx.
Is this the correct way to use useState() and useEffect() to fetch the data? I made multiple useState constants so after I can render the Title and the list op Posts in the render method:
function ArticleList() {
const [categoryTitle, setCategoryTitle] = useState('')
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
axios
.get('mock-data/my-data.json')
.then(res => {
console.log(res.data)
setCategoryTitle(res.data['title'])
setPosts(res.data.allItems)
setLoading(false)
})
.catch(err => {
console.log(err)
})
}, [])
if (loading) {
return <p>Loading articles...</p>
}
return (
<div>
<h1>{categoryTitle}</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.titel}</li>
))}
</ul>
</div>
)
}
Usually its a good idea to use useReducer hook instead of using multiple useState hooks if you want to set multiple states simultaneously. Your example is that case where you are trying to set multiple states together.
useReducer uses reducer function where consolidated state can be manipulated. This makes code more cohesive and more readable in my opinion.
Please see below sample code. For simplicity I have removed axios but you should get the idea.
https://codesandbox.io/s/bold-platform-hv6pi?file=/src/ArticleList.js
This looks good to me.
If I were to refactor this code, I would prefer to use async/await, and I like to use state enums rather than booleans, for loading/loaded/error states.
function ArticleList() {
const [categoryTitle, setCategoryTitle] = useState('');
const [posts, setPosts] = useState([]);
const [state, setState] = useState('loading');
useEffect(() => {
const fetchPosts = async () => {
setState('loading');
let response = axios.get('mock-data/my-data.json');
try {
response = await response;
} catch (error) {
setState('error');
return;
}
setCategoryTitle(response.data['title']);
setPosts(response.data.allItems);
setState('loaded');
};
fetchPosts();
}, []);
if (state === 'loading') {
return <p>Loading articles...</p>;
}
if (state === 'error') {
return <p>Error loading articles...</p>;
}
return (
<div>
<h1>{categoryTitle}</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.titel}</li>
))}
</ul>
</div>
);
}
Yup, this is the correct way. If you wanna use only one useState, you can clone the state object.
For example:
const [state, setState] = useState({
title: "",
posts: []
});
// For Title
setState({...state, title: "New Title"})
// For Posts
setState({...state, posts: [{...post...}]}
But I would like to use lodash clone, immer library or something like that.
This is pretty much it, the only thing that I would highly recommend is use await instead of the famous hadoukens/pyramids of doom, so a plain code would be:
function ArticleList() {
const [categoryTitle, setCategoryTitle] = useState('')
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
async function getData(){
try{
setLoading(true)
res = await axios.get('mock-data/my-data.json');
console.log(res.data)
setCategoryTitle(res.data['title'])
setPosts(res.data.allItems)
setLoading(false)
}catch(err){
console.log(err)
}
}
getData();
}, [])
if (loading) {
return <p>Loading articles...</p>
}
return (
<div>
<h1>{categoryTitle}</h1>
<ul>
{posts.map(post => (
<li key={post.id}>{post.titel}</li>
))}
</ul>
</div>
)
}

Can not change state of object [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 years ago.
I am trying to fetch a single object, and it fetches, I get correct data in response.data, but i can't set it to state. It just remains as a null object. What am I doing wrong?
Post is a json object with several fields like: post_id, post_title, post_content and etc.
const [post, setPost] = useState({})
let id = match.params.id
useEffect(() => {
axios.get(`${BASE_URL}/post?id=${id}`)
.then(response => {
console.log(response.data)
setPost(response.data)
})
.then(() => {
console.log("post: ", post)
}
)
setAction like your setPost are asynchronous, as stated in the official REACT documentation (https://reactjs.org/docs/hooks-reference.html#usestate); this means that, once the setAction is executed, you don't know when it will be actually executed and terminated: you will know because the component will re-render.
In your case, if you'd like to perform action AFTER post has got the new value, you would need the useEffect hook (https://reactjs.org/docs/hooks-reference.html#useeffect):
React.useEffect(() => {
console.log(post);
}, [post]);
By the way, I think you would want the data inside the response, so you would probably save the JSON retrieved from the body of the HTTP Response, that you can get using response.json().
EDIT: As stated in the comment from Siradji Awoual, what I wrote about response and response.json() is not valid for Axios (but it still is for fetch API).
Setting a state is asynchronous. That means you don't know exactly when that action will finish executing.
If I were you, I would use something like useEffect to check if the state is being set.
React.useEffect(() => console.log(post), [post])
Using axios.get is low-level and requires that you hook up a bunch of extra stuff to get things working correctly. Instead, try writing custom hooks to abstract this logic away -
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Using useAsync looks like this -
const MyApp = () => {
const { loading, error, result } =
useAsync(_ => axios.get("./foo.json").then(res => res.json()))
if (loading)
return <p>loading...</p>
if (error)
return <p>error: {error.message}</p>
return <pre>result: {result}</pre>
}
But you will probably have many components that fetch JSON, right? We can make an even higher level custom hook, useJSON that is a specialization of useAsync -
const fetchJson = (url = "") =>
axios.get(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") =>
useAsync(fetchJson, [url]) // <-- useAsync
const MyApp = () => {
const { loading, error, result } =
useJson("./foo.json") // <-- dead simple
if (loading)
return <p>loading...</p>
if (error)
return <p>error: {error.message}</p>
return <pre>result: {result}</pre>
}
See the custom hooks in action in this functioning code snippet -
const { useState, useEffect } =
React
// fake fetch slows response down so we can see loading
const _fetch = (url = "") =>
fetch(url).then(x =>
new Promise(r => setTimeout(r, 2000, x)))
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [result, setResult] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
const fetchJson = (url = "") =>
_fetch(url).then(r => r.json())
const useJson = (url = "") =>
useAsync(fetchJson, [url])
const MyComponent = ({ url = "" }) => {
const { loading, error, result } =
useJson(url)
if (loading)
return <pre>loading...</pre>
if (error)
return <pre style={{color: "tomato"}}>error: {error.message}</pre>
return <pre>result: {JSON.stringify(result, null, 2)}</pre>
}
const MyApp = () =>
<main>
ex 1 (success):
<MyComponent url="https://httpbin.org/get?foo=bar" />
ex 2 (error):
<MyComponent url="https://httpbin.org/status/500" />
</main>
ReactDOM.render(<MyApp />, document.body)
pre {
background: ghostwhite;
padding: 1rem;
white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

async custom hooks does't provide updated data

I have a simple hook to help me handle a POST request. With the following code, I expect unsub will be true after the POST is done. Can anyone point out anything I could have done wrong?
Custom Hook
const useUnsubscribeEmail = () => {
const [userId, setUserId] = useState(null);
const [unsub, setUnSub] = useState();
const UNSUB_URL = '/web-registry-api/v1/reviews/unsubscription';
useEffect(() => {
if (userId) {
// async POST call
(async () => {
try {
await ApiService.post(`${UNSUB_URL}/${userId}`);
// update unsub value
setUnSub(true);
} catch (error) {
console.error(error)
}
})();
}
}, [userId]);
return [unsub, setUserId];
};
export default useUnsubscribeEmail;
Component
const ReviewUnsubscription = () => {
const { userId } = useParams();
const [unsub, unsubscribeEmail] = useUnsubscribeEmail();
return (
<MinimumLayout>
<div className={styles.content}>
<h1>Unsubscribe from email reminders to review products you’ve received from Zola?{unsub}</h1>
{/* unsub here is still undefined */}
<Button disabled={unsub} onClick={() => { unsubscribeEmail(userId); }} variant="primary" className={styles.button}>Unsubscribe</Button>
</div>
</MinimumLayout>
);
};
unsub is still going to be undefined until you click the button as you have not set a default state for it in your hook.
change : const [unsub, setUnSub] = useState(); to const [unsub, setUnSub] = useState(false); is what I would recommend
I tested on my side and works just fine; However, I cannot test the APIService.post.

data coming from custom fetch hook and local state

I want to have a local setter to update data I'm extracting from custom fetch hook:
function ContactList(props) {
const contacts =useFetch(fetchUrl, []);**//contain data**
const [data,setData] = useState(contacts);**//remains empty**
return (
<div>
<Navbar />
<ul className="collection">
<ContactItem contacts={contacts} setContacts={setData} />
</ul>
</div>
);
}
fetchHook looks like this:
export const useFetch = (url, initialValue) => {
const [result, setResult] = useState(initialValue);
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setResult(result.data.contact);
};
fetchData();
}, []);
return result
}
so I need the contactList to have a setter which I currently don't manage to achieve because state remain an empty array.
const contacts =useFetch(fetchUrl, []);
useFetch is asynchronous and makes the call and gives the result after the componentMounts, since I'm assuming you put that in a useEffect with empty dependency in your hook.
So, the data won't be available on the first render, making it initialize to empty.
EDIT:
To understand better, follow the console.log in the following example:
https://codesandbox.io/embed/friendly-ives-xt5k0
You'll see that it is empty first and then it gets the value.
Now, the same is the case in your code. On the first render it's empty and that is when you set the state of result. Instead, you can just use the value of contacts in your case.
EDIT 2:
After seeing what you need, all you have to do is to also return the setResult from the hook:
export const useFetch = (url, initialValue) => {
const [result, setResult] = useState(initialValue);
useEffect(() => {
const fetchData = async () => {
const result = await axios(url);
setResult(result.data.contact);
};
fetchData();
}, []);
return {result, setResult}
}
function ContactList(props) {
const {result, setResult} =useFetch(fetchUrl, []);
return (
<div>
<Navbar />
<ul className="collection">
<ContactItem contacts={result} setContacts={setResult} />
</ul>
</div>
);
}

Resources