im trying to build a weather app and i have been defined buttons to change the city name state and fetch the new city weather info by changing the dependency list of the useEffect hook and console log it for checking frst. but the problem is that the state is updating two steps behind the buttons that i click.
Main component for states and fetching data:
function App() {
const [cityName, setCityName] = useState('Toronto')
const [weather, setWeather] = useState('')
const url = `https://api.openweathermap.org/data/2.5/weather?q=${cityName}&units=metric&appid=5476766e86450fff39da1502218a3376`
const getData = () => {
axios.get(url).then((res) => {
setWeather(res.data);
});
console.log(weather)
}
useEffect(()=>{
getData()
},[])
return <div className="bg-slate-600">
<Topbuttons setCityName={setCityName} getData={getData} />
<Inputs setCityName={setCityName} getData={getData} />
<Data weather={weather} />
</div>;
}
export default App;
Im sending the setCity to the buttons component as a prop.
nevermind the getData() being repeated in useEffect its just for test
buttons component:
function Topbuttons({setCityName,getData}) {
const cities = ['Tehran', 'Paris', 'Newyork', 'Berlin', 'London']
return (
<div className='flex mx-auto py-3 justify-around space-x-8 items-center text-white text-l font-bold'>
{cities.map((v,i) => {
return <button
key={i}
onClick={()=>{
console.log(v)
setCityName(v)
getData()
}}>
{v}
</button>
})}
</div>
)
}
export default Topbuttons
thanks for your help
The problem is that you run setCityName() immediately on click but you run setWeather() after the promise is complete with data. One way to sync them is to run them at the same time after the async promise completes.
Here's a working sandbox of your code. Try using this setup:
app.js
import axios from "axios";
import { useEffect, useState } from "react";
import TopButtons from "./TopButtons";
function App() {
const [cityName, setCityName] = useState("Toronto");
const [weather, setWeather] = useState("");
const getData = (newCityName) => {
console.log("getData newCityName", newCityName);
const url = `https://api.openweathermap.org/data/2.5/weather?q=${newCityName}&units=metric&appid=5476766e86450fff39da1502218a3376`;
axios.get(url).then((res) => {
setCityName(newCityName);
setWeather(res.data);
});
};
useEffect(() => {
getData();
}, []);
return (
<div className="bg-slate-600">
<TopButtons setCityName={setCityName} getData={getData} />
<span>{JSON.stringify(weather)}</span>
</div>
);
}
export default App;
TopButtons.js
function Topbuttons({ getData }) {
const onClick = (newCityName) => getData(newCityName);
const cities = ["Tehran", "Paris", "Newyork", "Berlin", "London"];
return (
<div className="flex mx-auto py-3 justify-around space-x-8 items-center text-white text-l font-bold">
{cities.map((v, i) => {
return (
<button key={i} onClick={() => onClick(v)}>
{v}
</button>
);
})}
</div>
);
}
export default Topbuttons;
The next step would be to detect for a loading state. Since you're using Axios + React, I recommend using the axios-hooks library which uses Axios under-the-hood but provides loading state in hook-format which makes API calls easier to deal with.
Related
I'm trying to get a handle on the new tRPC version 10 with a basic shopping list CRUD Nextjs app. I have successfully set up the tRPC endpoint with "get all" and a "create" handlers and can confirm that they both work after testing from the front end. However, I can't seem to update my state with the data from the "get all" call. In older tRPC versions we would have updated the state as follows:
const data = trpc.useQuery(["items.getAll"], {
onSuccess(items) {
setItems(items);
},
});
In version 10 however, they've done away with the useQuery() arguments in favour of conditional status returns according to docs. I tried updating the state as follows:
const [items, setItems] = useState<ShoppingItem[]>([]);
const data = trpc.shoppingItem.getAll.useQuery();
if (data.isSuccess) {
setItems(data.data);
}
This understandably causes a "Too many re-renders" error since each time the state updates it re-renders the component, therefore triggering a new isSuccess and re-updating the state.
What is the proper way to update state from tRPCv10?
My full component follows for context:
import { useState, useEffect } from "react";
import { ShoppingItem } from "#prisma/client";
import type { NextPage } from "next";
import Head from "next/head";
import ItemModal from "../components/ItemModal";
import { trpc } from "../utils/trpc";
const Home: NextPage = () => {
const [items, setItems] = useState<ShoppingItem[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const data = trpc.shoppingItem.getAll.useQuery();
if (data.isSuccess) {
setItems(data.data);
}
return (
<>
<Head>
<title>Shopping List</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
{modalOpen && (
<ItemModal setModalOpen={setModalOpen} setItems={"hello"} />
)}
<main className="mx-auto my-12 max-w-3xl">
<div className="flex justify-between">
<h2 className="text-2xl font-semibold">My Shopping List</h2>
<button
className="rounded-md bg-violet-500 p-2 text-sm text-white transition hover:bg-violet-600"
type="button"
onClick={() => setModalOpen(true)}
>
Add Item
</button>
</div>
<ul className="mt-4">
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</main>
</>
);
};
export default Home;
I had the same problem and managed to get it to work by adding 'undefined' as an argument to useQuery().
const { data: itemsData, isLoading } = trpc.items.getAllItems.useQuery(undefined, {
onSuccess(items) {
setItems(items)
}
})
Not sure if this is the correct way to update state in tRPC v10 but it seems to be working. Let me know if you find the correct way to do it!
I'm not sure if this is the intended way to deal with this but I managed to get it working as follows:
const [items, setItems] = useState<ShoppingItem[]>([]);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const { data: shoppingItems } = trpc.shoppingItem.getAll.useQuery();
useEffect(() => {
shoppingItems && setItems(shoppingItems);
}, [shoppingItems]);
I call useEffect to update the state when there is a change in the data pulled from the tRPC call.
Additionally, I pass down the setItems() method to my form component and call that on the success of the mutation:
const [input, setInput] = useState<string>("");
const addItem = trpc.shoppingItem.addItem.useMutation({
async onSuccess(newItem) {
setItems((prev) => [...prev, newItem]);
console.log(newItem);
},
});
const handleAddItem = () => {
addItem.mutate({ name: input });
setModalOpen(false);
};
What I want is when I click on:
let Afeef = `/${category}`
<Link to={Afeef} className='products-categories'> <h4>{category}</h4></Link>
It should change products according to URL which could be "/electronics","/jewelry" etc but the problem I am facing is that it is changing my URL but the products are not changing. I can't understand what is the problem here. I tried different things but I cant understand it.
import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom';
import './Allproducts.css'
import Categories from './categories.json'
import ForYouItem from './ForYouItem'
export default function Allproducts(props) {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch(`https://fakestoreapi.com/products/category/${props.category}`)
.then((res) => res.json())
.then((data) => setProducts(data))
}, [])
const [categories, setCategories] = useState([])
const updateCategory = async ()=> {
const url = "./categories.json"
let data = await fetch(url);
let parsedData = await data.json()
setCategories(parsedData.title)
}
useEffect(() => {
updateCategory();
}, [])
return (
<>
<div className="banner">
<h1>Afeef</h1>
<h4>Get best items in budget</h4>
</div>
<div className="main-grid">
<div className="left-grid">
<div className="left-categories">
<h1>Collections</h1>
{categories.map((category) => {
let Afeef = `/${category}`
return (
<Link to={Afeef} className='products-categories'> <h4>{category}</h4></Link>
)
}
)}
</div>
</div>
<div className="right-grid">
<div className="row ">
{products.map((product) => {
return (
<div className="col-md-4 my-2 Products-1">
<ForYouItem Title={product.title.slice(0, 50)} Price={product.price} Imageurl={product.image} rate={product.rating.rate} count={product.rating.count} />
</div>
)
}
)}
</div>
</div>
</div>
</>
)
}
im gonna try to explain what i understand from your code. So based on the flow of your code, the product can only be fetch once when the page loaded.
but i think in your useEffect that fetch product, you can add the state of Categories in the bracket "[categories]".
then add an onclick setstate product in your link.
so when you click your link, the categories state is updated. then because the bracket inside useEffect that have [categories] is updated the useEffect is run. hence fething new product.
apparently, it is considered a best practice if you can split all the data fetching functions to a separate folder called services and put all the functions there then use them in the project
However, I couldn't make it happen with Next.js
The function works when I put it inside the same component I want to render the data inside it, but doesn't work when I put the function in a separate folder and export it.
I get this error:
TypeError: Cannot read properties of undefined (reading 'map')
I think it is because the data props are not being passed to the component correctly.
below are the working and none working examples :
the Working example:
inside index.tsx file :
export async function getStaticProps() {
const response = await fetch(
`https://api.github.com/users/abdurrahmanseyidoglu/repos`
);
const data = await response.json();
if (!data) {
return {
notFound: true,
};
}
return {
props: {
data,
}, // will be passed to the page component as props
};
}
const Home = ({ data }) => {
return (
<>
<ul>
{data.map((repo) => (
<li key={repo.id}>{repo.name}</li>
))}
</ul>
<p className="max-w-5xl m-auto text-justify mt-2 border rounded p-3 shadow-lg">
Working Example
</p>
</>
);
};
export default Home;
Not Working example:
inside services/getUserRepos.ts :
export async function getStaticProps() {
const response = await fetch(
`https://api.github.com/users/abdurrahmanseyidoglu/repos`
);
const data = await response.json();
if (!data) {
return {
notFound: true,
};
}
return {
props: {
data,
},
};
}
inside index.tsx file :
import { getStaticProps } from "../services/getUserRepos";
const Home = ({ data }) => {
return (
<>
<ul>
{data.map((repo) => (
<li key={repo.id}>{repo.name}</li>
))}
</ul>
<p className="max-w-5xl m-auto text-justify mt-2 border rounded p-3 shadow-lg">
None Working Example
</p>
</>
);
};
export default Home;
How can I make it so I can pass the fetched data from a separate folder to my index.tsx file
inside services/user.service.ts :
export async function getUserRepos() {
const response = await fetch(
`https://api.github.com/users/abdurrahmanseyidoglu/repos`
);
const data = await response.json();
return data;
}
inside index.tsx file :
import { getUserRepos } from "../services/user.service";
export async function getStaticProps() {
const data = await getUserRepos();
if (!data) {
return {
notFound: true,
};
}
return {
props: {
data,
}, // will be passed to the page component as props
};
}
const Home = ({ data }) => {
return (
<>
<ul>
{data.map((repo) => (
<li key={repo.id}>{repo.name}</li>
))}
</ul>
<p className="max-w-5xl m-auto text-justify mt-2 border rounded p-3 shadow-lg">
Working Example
</p>
</>
);
};
export default Home;
You are not using getStaticProps after importing it, your code should look like this
import {getStaticProps} from "../services/getUserRepos";
import {useState} from "react";
const Home = () => {
const [userRepos, setUserRepos] = useState([]);
getStaticProps().then((data) => setUserRepos(data));
return (
<>
<ul>
{userRepos.map((repo) => (
<li key={repo.id}>{repo.name}</li>
))}
</ul>
<p className="max-w-5xl m-auto text-justify mt-2 border rounded p-3 shadow-lg">
None Working Example
</p>
</>
);
};
export default Home;
I am trying to create a history page with react hooks that keeps track of the users most recent searches they don't have to be persistent through refreshes only from this session.
my search component looks like this this is a simple app that does not need a UI just a simple navigation on the search page it will show the results and on the history page I would like to be able to show the previous searches from this session
I am trying to keep track of the debouncedTerm so I can display it in a new component
import React, { useState, useEffect } from 'react';
import axios from 'axios';
const Search = () => {
const history = [];
const [term, setTerm] = useState('');
const [debouncedTerm, setDebouncedTerm] = useState(term);
const [results, setResults] = useState([]);
useEffect(() => {
const timerId = setTimeout(() => {
setDebouncedTerm(term);
}, 1000);
return () => {
clearTimeout(timerId);
};
}, [term]);
useEffect(() => {
const search = async () => {
const { data } = await axios.get('http://hn.algolia.com/api/v1/search?', {
params: {
query: debouncedTerm,
},
});
setResults(data.hits);
};
if (debouncedTerm) {
search();
}
}, [debouncedTerm]);
const renderedResults = results.map((result) => {
return (
<div key={result.objectID} className="item">
<div className="right floated content">
<a className="ui button" href={result.url}>
Go
</a>
</div>
<div className="content">
<div className="header">{result.title}</div>
</div>
</div>
);
});
return (
<div>
<div className="ui form">
<div className="field">
<label>Hacker News Search:</label>
<input
value={term}
onChange={(e) => setTerm(e.target.value)}
className="input"
/>
</div>
</div>
<div className="ui celled list">{renderedResults}</div>
</div>
);
};
export default Search;
Your code looks like it's going in the right direction but you have a constant const history = []; and you must keep in mind that this will not work, because you will have a new constant re-declared in every render. You must use React setState like so:
const [history, setHistory] = useState([]);
You can read more about it in the React documentation.
edit:
In order to add new elements to the existing history you have to append it like this:
setHistory(oldHistory => [...oldHistory, newHistoryElement]);
I have a social media app that I would like to function like facebook, where you comment and the comment is loaded in real-time with out having to refresh your browser to display the comment. I am able to send data from React to backend server and I am able to get that data with a axios http request, but I have to refresh the browser to see the comment displayed. I am also see the comment display more then once. I am not getting any errors but the comment is not unique to the post, as it is an array that loads the posts. Did I make a mistake in my code?
Here is the front end code.
import React, { useState, useEffect } from "react";
import Container from "react-bootstrap/Container";
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
import axios from "axios";
import "./css/sharewall.css";
const ComponentName = () => {
const [posts, setPosts] = useState([]);
const [comment, setComment] = useState("");
const [id, setId] = useState("");
const loadData = async () => {
try {
let res = await axios.get(`http://localhost:5000/getall`);
setPosts(res.data);
} catch (error) {
console.log(error);
}
};
function makeRequest(e) {
e.preventDefault();
axios({
method: "POST",
url: "http://localhost:5000/postinput",
data: {
comment: comment,
},
}).then((res) => {
setComment(res.data.comment);
console.log(res.data);
});
}
const loadComment = async () => {
try {
let res = await axios.post("http://localhost:5000/postinput");
setComment(res.data.comment._id);
console.log(res.data.comment._id)
} catch (error) {
console.log(error);
}
};
useEffect(() => {
loadData();
}, []);
return (
<div className="compoentclass">
<Container className="mt-5 ml-auto mr-auto">
<div className="text-center">
{posts.map((post, index) => (
<div>
<Card className="">
<Card.Img alt="" src={post.url} />
<Card.ImgOverlay className="overlay">
<Card.Title className="text-center mt-5">
<Card.Text className="cardStyle text-light">
{post.body}
</Card.Text>
</Card.Title>
</Card.ImgOverlay>
</Card>
{posts.map((post, index) => (
<div><Card.Text>{post.comment}</Card.Text></div>
))}
<textarea
className="comment text-center mt-3 mb-3"
onChange={e => setComment(e.target.value)}
value={comment}
name={"comment"}
type={"text"}
/>
<div className="d-flex justify-content-start mt-n3 mb-4">
<Button
className="shareButton"
variant="secondary"
onClick={makeRequest}
onChange={loadComment}
>
Comment
</Button>
</div>
</div>
))}
</div>
</Container>
</div>
);
};
export default ComponentName;
Here is the render from the comments, the comments double or tripple.
In order for other users (the user that posts a comment should be easily able to see the comment immediately) to see the comments real-time, you must implement some sort of "listener" to the server/database to listen for new comments. Otherwise, how should my browser know that YOU posted a comment just now? Check out socket.io, it is quite easy to implement.
I've added some additions to your code, see comments.
First, it seems you can use useEffect to rerender your comments every time you will click the "comment" button. To handle updates you can create a new state as I did.
Probably you are having troubles with multi comments because your posts array contains more than one element inside. Inside render it maps through all posts array and displays every element.
Also, would be better if you will recreate your code inside codesandbox.io or similar.
import React, { useState, useEffect } from "react";
import Container from "react-bootstrap/Container";
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
import axios from "axios";
import "./css/sharewall.css";
const ComponentName = () => {
const [posts, setPosts] = useState([]);
const [comment, setComment] = useState("");
const [id, setId] = useState("");
//state for resending load request
const [isCommentFetched, setCommentFetched] = useState(false);
const loadData = async () => {
try {
let res = await axios.get(`http://localhost:5000/getall`);
setPosts(res.data);
} catch (error) {
console.log(error);
}
};
function makeRequest(e) {
e.preventDefault();
axios({
method: "POST",
url: "http://localhost:5000/postinput",
data: {
comment: comment,
},
}).then((res) => {
setComment(res.data.comment);
setCommentFetched(true)
console.log(res.data);
})
//don't forget to catch errors
.catch((err)=>{
console.log(err)
})
}
const loadComment = async () => {
try {
let res = await axios.post("http://localhost:5000/postinput");
setComment(res.data.comment._id);
console.log(res.data.comment._id);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
loadData();
}, []);
//hook fires whenever your isCommentFetched state updating.
useEffect(() => {
// if isCommentFetched true, it will send request for get new posts, and will update your comments in render.
if(isCommentFetched){
loadData();
}
}, [isCommentFetched]);
return (
<div className="compoentclass">
<Container className="mt-5 ml-auto mr-auto">
<div className="text-center">
{posts.map((post, index) => (
<div>
<Card className="">
<Card.Img alt="" src={post.url} />
<Card.ImgOverlay className="overlay">
<Card.Title className="text-center mt-5">
<Card.Text className="cardStyle text-light">
{post.body}
</Card.Text>
</Card.Title>
</Card.ImgOverlay>
</Card>
{posts.map((post, index) => (
<div>
<Card.Text>{post.comment}</Card.Text>
</div>
))}
<textarea
className="comment text-center mt-3 mb-3"
onChange={(e) => setComment(e.target.value)}
value={comment}
name={"comment"}
type={"text"}
/>
<div className="d-flex justify-content-start mt-n3 mb-4">
<Button
className="shareButton"
variant="secondary"
onClick={makeRequest}
onChange={loadComment}
>
Comment
</Button>
</div>
</div>
))}
</div>
</Container>
</div>
);
};
export default ComponentName;