SetState Not Updating to fetch dynmically - javascript

I am learning react and I am trying to use a text input in a dynamic fetch request
My component is defined as ...
export default testpage = () => {
const [state, setState] = React.useState({})
let handleChange = (event) => {
setState({input: event.target.value})
}
async function buttonClick (input) {
console.log(state.input)
await fetch(`http://localhost:8080/api/${input}`)
.then(response => response.json())
.then(data => setState({...state, data}))
render(
<input type={'text'} onChange={handleChange.bind(this)} />
<Button onClick={() => buttonClick(state.input)}>test</Button>
)
}
My problem relates to useState updating asynchronously. If I enter a number ie. 4 into the input box and then click the button. The first time I click the button the fetch fails because undefined is passed to the fetch statement because the state hasn't been updated. If I click the button a second time the fetch succeeds. I have read into the useEffect hook but I am unable to figure out how to apply it to my situation.

Change the code to keep input's value directly in the state. The state value not need to be an object - it can be a string, number or null if that’s all you need.
const TestPage = () => {
const [postId, setPostId] = useState(null);
async function buttonClick() {
await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`)
.then(response => response.json())
.then(data => console.log(data));
}
return (
<div>
<input onChange={e => setPostId(e.target.value)} />
<button onClick={buttonClick}>test</button>
</div>
);
};
The comonent already works as expected - it downloads data on every button click. It requires a display logic and a proper error handling, but I leave it for clarity.
You mentioned useEffect and here is the example of how you can use it:
function Test() {
const [postId, setPostId] = useState(null);
const [data, setData] = useState([]);
useEffect(() => {
async function getComments() {
if (Number(postId)) {
await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}/comments`
)
.then(response => response.json())
.then(data => setData(data));
} else { setData([]); }
}
getComments();
}, [postId]);
const comments = data
? data.map(comment => <li key={comment.id}>{comment.body}</li>)
: [];
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<input type={"text"} onChange={e => setPostId(e.target.value)} />
{comments.length > 0 ? <ul>{comments}</ul> : <span>Write correct post ID (number 1-100)</span>}
</div>
);
}
But useEffect changes how you interact with your component. It runs an effect after rendering new state, meaning it runs right after changing input's value. Meaning, you don't need the <button> at all.
Because you begin request on button click it is better to use useCallback hook. It returns the same function on every button click as long as postId (input's value) doesn't change. You can use this function the same as you used buttonClick in first example:
const TestPage = () => {
const [postId, setPostId] = useState(null);
const handleClick = useCallback(() => {
async function getData() {
await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}/comments`
)
.then(response => response.json())
.then(data => console.log(data));
}
getData();
}, [postId]);
return (
<div>
<input onChange={e => setPostId(e.target.value)} />
<button onClick={handleClick}>test</button>
</div>
);
};

Related

Fetch not working on first click of onClick function

When I use modalOpen in a onClick function it wont fetch api on the 1st click causing the code to break but it will on 2nd click what can cause it
// get anime result and push to modal function
const modalAnime = async () => {
const { data } = await fetch(`${base_url}/anime/${animeId}`)
.then((data) => data.json())
.catch((err) => console.log(err));
SetAnimeModalData(data);
};
I am trying to get the fetch to work on the first click but it doesn't until second or third click
const modalOpen = (event) => {
SetAnimeId(event.currentTarget.id);
SetModalVisible(true);
modalAnime();
console.log(animeId);
};
const modalClose = () => {
SetModalVisible(false);
SetAnimeId("");
};
return (
<div className="app">
<Header
searchAnime={searchAnime}
search={search}
SetSearch={SetSearch}
mostPopular={mostPopular}
topRated={topRated}
/>
{loadingState ? (
<ResultLoading />
) : (
<Results
animeResults={animeResults}
topRated={topRated}
mostPopular={mostPopular}
modalOpen={modalOpen}
/>
)}
{modalVisible ? <AnimeInfoModal modalClose={modalClose} /> : <></>}
</div>
);
the modal opens fine but the ID isn't captured until the second or third click
I have more code but Stack Overflow won't let me add it.
SetAnimeId() won't update the animeId state property until the next render cycle. Seems like you should only update the visible and animeId states after you've fetched data.
You should also check for request failures by checking the Response.ok property.
// this could be defined outside your component
const fetchAnime = async (animeId) => {
const res = await fetch(`${base_url}/anime/${animeId}`);
if (!res.ok) {
throw res;
}
return (await res.json()).data;
};
const modalOpen = async ({ currentTarget: { id } }) => {
// Await data fetching then set all new state values
SetAnimeModalData(await fetchAnime(id));
SetAnimeId(id);
SetModalVisible(true);
};

Load More with paginated api

I'm making a Blog Web Page and the API I'm using to build it, it's already paginated so it returns 10 objects in every request I make.
But the client wants the page to have a "load more" button, in each time the user click on it, it will keep the already loaded data and load more 10 objects.
So far, I've made the button call more 10 new objects, everytime I clicked on it but I also need to keep the already loaded data.
This is my file so far:
MainPage.js
import React, { useState, useEffect} from 'react';
const MainPage = () => {
const [blogs, setBlogs] = useState('');
const [count, setCount] = useState(1);
useEffect(() => {
fetch("https://blog.apiki.com/wp-json/wp/v2/posts?_embed&categories=518&page="+count)
.then((response) => response.json())
.then((json) => {
console.log(json)
setBlogs(json)
})
}, [count])
const clickHandler = () => {
console.log(count);
return setCount( count+1)
}
return (
<div>
<p>All the recent posts</p>
{ blogs && blogs.map((blog) => {
return (
<div key={blog.id}>
<img width="100px" src={blog._embedded["wp:featuredmedia"][0].source_url}/>
<p>{blog.title["rendered"]}</p>
</div>
)
})
}
<button onClick={clickHandler}>LoadMore</button>
</div>
)
}
export default MainPage;
The idea is pretty simple. Just concatenate arrays using the Spread syntax as follows.
var first =[1, 2, 3];
var second = [2, 3, 4, 5];
var third = [...first, ...second];
So, do this thing when you're clicking the load more button.
Here I've come up with handling the whole thing:
Firstly, I will call a function inside the useEffect hook to load some blog posts initially. Secondly I've declared an extra state to show Loading and Load More text on the button.
Here is the full code snippet:
import React, { useState, useEffect } from "react";
const MainPage = () => {
const [loading, setLoading] = useState(false);
const [blogs, setBlogs] = useState([]);
const [count, setCount] = useState(1);
useEffect(() => {
const getBlogList = () => {
setLoading(true);
fetch(
"https://blog.apiki.com/wp-json/wp/v2/posts?_embed&categories=518&page=" +
count
)
.then((response) => response.json())
.then((json) => {
setBlogs([...blogs, ...json]);
setLoading(false);
});
};
getBlogList();
}, [count]);
return (
<div>
<p>All the recent posts</p>
{blogs &&
blogs.map((blog) => {
return (
<div key={blog.id}>
<img
width="100px"
src={blog._embedded["wp:featuredmedia"][0].source_url}
/>
<p>{blog.title["rendered"]}</p>
</div>
);
})}
{
<button onClick={() => setCount(count + 1)}>
{loading ? "Loading..." : "Load More"}
</button>
}
</div>
);
};
export default MainPage;
According to React documentation:
If the new state is computed using the previous state, you can pass a function to setState.
So you could append newly loaded blog posts to the existing ones in useEffect like this:
setBlogs((prevBlogs) => [...prevBlogs, ...json])
I would also set the initial state to an empty array rather than an empty string for consistency:
const [blogs, setBlogs] = useState([]);

How can I render some JSX with onClick event?

So I'm trying to render some JSX when a user presses a button, I'm trying to do this via onClick but it is not working, The JSX does not get rendered on the screen. Is there a way to do this? My code is like this:
function RandomScreen() {
async function HandleClick() {
// make API post request
.then(function(response) {
return (<h1>{response.data}</h1>)
})
}
return (
<button onClick={HandleClick}>Click me</button>
)
}
I think you are looking for something like this:
function RandomScreen() {
const [data, setData] = useState(null);
async function HandleClick() {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((json) => {
console.log(json);
setData(json);
});
}
return (
<button onClick={HandleClick}>
Click me
{data && <h1>{JSON.stringify(data, null, 2)}</h1>}
</button>
);
}
store your data in a state for example
const [data, setData] = React.useState();
...
.then(function(response) {
setData(response.data);
})
and in JSX you can handle the display part:
{data && <h1>{data}</h1>}

Can I use two useEffect and have map inside a map

I am new to React and would like some help with the following problem. I current have this code.
import React, { useState, useEffect } from "react";
function FetchData() {
const [repos, setRepos] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch("https://api.github.com/orgs/org_name/repos")
.then((res) => res.json())
.then((data) => {
setRepos(data);
})
.then(() => {
setIsLoading(false);
})
.catch((err) => console.log(err));
}, []);
return (
<div>
{repos.map((repo) => (
<div key={repo.id}>
<div>
<h2>Name: {repo.name}</h2>
<p>Top 5 Contributors</p>
))}
My above codes work fine, but my problem now is that I would like to add the top 5 contributors to the repository and to access that I have to go to https://api.github.com/repos/org_name/{repos}/contributors, and to get to that, I first have to use repo.contributor_url Should I use another useEffect and map to show the top 5 contributors?
Edit
Basically I want to do something like this.
useEffect(() => {
fetch(`${repos.contributors_url}`)
.then((res) => res.json())
.then((data) => {
setContributors(data);
console.log(data);
})
.catch((err) => console.log(err));
}, []);
...
<p> Top 5 Contributors: </p>
<ul>
{contributors.map((c, i) => {
<li key={i}>{c.name}</li>
)}
</ul>
Since you are new to React. React used to have class based components to handle state and those class based components had special functions called- Life-Cycle-Methods. But from React 16.8 onwards React Community came up with React-Hooks and functional components can now be used to handle state and useState() and useEffect() are examples of Hooks.
Now useEffect() alone is used to do perform life-cycle method's work.
The way you have used useEffect() in your code is simulating componentDidMount() as you have kept the 2nd argument as an empty array []
We can use other life-cycle methods like componentDidUpdate() and componetnWillUnmount() using useEffect() Hook itself.
Then based on your requirement you can use useEffect() Hook as many times as required by your Component.
Coming to Updated part of your question now:
So, you basically need to do promise chaining. We know that fetch() is promise based,so when one asynchronous call is resolved and we get the first data, within your useEffect() hook only, you need to make another asynchronous request using the second url-end point to get the respective data.
Here is the updated code now: Try this
import React, { useState, useEffect } from 'react';
function FetchData() {
const [repos, setRepos] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [contributors, setContributors] = useState([]);
const [isContributorLoading, setIsContributorLoading] = useState(true);
useEffect(() => {
fetch('https://api.github.com/orgs/{org}/repos')
.then((res) => res.json())
.then((data) => {
setRepos(data); // Data 1(repos) is received
// Now We make another API call to get Data 2 (contributors)
return fetch('https://api.github.com/repos/{org}/{repos}/contributors');
})
.then((res) => res.json()) // Chaining promise,handling 2nd Fetch request
.then((data2) => {
console.log(data2);
setContributors(data2);
})
.then(() => {
setIsLoading(false);
})
.catch((err) => console.log(err));
}, []);
return (
<div>
{ repos.length && repos.map((repo) => (
<div key={repo.id}>
<div>
<h2>Name: {repo.name}</h2>
</div>
</div>
))}
<p> Top 5 Contributors: </p>
<ul>
{contributors.length && contributors.map((c, i) => {
return <li key={i}>{c.name}</li>
)}
</ul>
</div>
);
}
So, basically you need to learn a bit more about how to use Hooks especially useEffect(), for now. Do some googling stuff, It would not be good if I tell you everything now. Give it a shot then.
You can directly call apis inside one useEffect.
import React, { useState, useEffect } from "react";
function App() {
const [repos, setRepos] = useState([]);
const [contributor, setContributor] = useState([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function caller() {
try {
setIsLoading(true);
const response = await fetch(
"https://api.github.com/orgs/octokit/repos"
);
const result = await response.json();
const contri = [];
console.log(result);
result.forEach((item) => {
contri.push(fetch(`${item.contributors_url}`));
});
Promise.all(contri)
.then((contributorResults) => contributorResults)
.then((responses) => {
console.log(responses);
return Promise.all(responses.map((r) => r.json()));
})
.then((cont) => {
setContributor([...cont])
});
setRepos(result);
} catch (err) {
console.log(err);
} finally {
setIsLoading(false);
}
}
caller();
}, []);
return (
<div>
{repos.map((repo,index) => (
<div key={repo.id}>
<h2> Name: {repo.name} </h2>
{ contributor[`${index}`] && contributor[`${index}`].slice(0,5).map(item => {
return <div key={item.id}>
<div>{item.login}</div>
</div>
})}
</div>
))}
{isLoading && <div>...loading</div>}
</div>
);
}
export default App;

React setState inside useEffect wont work first call

I onpress method triggered the pressed useEffect api using promise in useEffect and according to incoming data set myDataStt and assign data to getData function I want to take action accordingly but I logged to getData func. triggered useEffect myDataStt wont change, press the buton second time this time succes myDataStt is change but first time press the button wont work (use effect triggered and value is correct) I dont understand why, thanks for comments
const [pressed, setPressed] = useState(false);
const [myDataStt, setMyData] = useState(announcements);
useEffect(() => {
CallApi.then(
values => {
setMyData(values);
const data = getData();
}),
}, [pressed]);
const getData = () => {
return myDataStt.dataFirst || [];
};
<Button
onPress={() => {
setPressed(true);
}}>
You shouldn't use useEffect in manner. Have your button actually perform the API call. If you want the API to load data on the component mount, then have it called in useEffect and on click. There is no need for a pressed state. You can assign your function to a callback, so it doesn't trigger an infinite loop.
const getData = useCallback(async () => {
const values = await CallApi();
setMyData(values);
}, [setMyData]);
useEffect(()=> {
getData();
}, [getData])
<Button onClick={() => getData()}>Refresh API</Button>
Look if this complete component works for you.
import React, { useState, useEffect } from 'react';
const CallAPI = () => {
//do your request/fetch
};
export const Component = () => {
const [data, setData] = useState([]);
useEffect(() => {
const initialCall = async () => {
const values = await CallAPI();
setData(values);
};
initialCall();
return () => {
//clean your fetch
};
}, []);
const handleClick = async () => {
const values = await CallAPI();
setData(values);
};
return (
<>
<button onClick={handleClick}>Add</button>
{data.map(item => {
return <div>{item}</div>;
})}
</>
);
};
2 remarks, you dont need 2 states for call your api, try to use onClick in buttons.

Categories

Resources