SWR Keep getting the same data through the loop - reactjs

I have home component and i passing props into Product card component
{products.map((product: ProductListStoreType) => (
<ProductCard
key={product.slug}
product={product}
category={product.category}
/>
))}
</div>
Category is just string _id from mongo db
and inside the product card
export default function ProductCard({ product, category }: ProductCardProps) {
const getCategory = async () => {
const { data } = await axios.get(`/api/category/${category}`);
return data.name;
};
const { data } = useSWR('/api/category', getCategory);
console.log(data);
The problem is SWR returns the same data for every loop. the id category is unique because it is from the _id mongo DB. but SWR keeps returning the same data which is from the category _id from the first loop
awlays return the first data for every map

The first parameter of useSWR is a unique key identifying the request. Since the key you are passing does not change (it's always '/api/category'), the data is not refreshed.
The key will be as the first argument to your fetcher, see the docs on arguments.
With these two things in mind, you could do something like this:
const getCategory = async (path) => {
const { data } = await axios.get(path);
return data.name;
};
export default function ProductCard({ product, category }: ProductCardProps) {
const { data } = useSWR(`/api/category/${category}`, getCategory);

Related

Is there a way to update context's state concurrently from multiple components in React?

I'm building a screen to display products available for sale. At the same time, each product would show its favorited status by the logged in user. To do this, each product loads its corresponding favoriteItem data from the backend.
If successful, 2 things happen:
1-the data is added to an array which is a state in a global context
2- the color of the heart is updated based on the state(by finding it)
The problem is the state always starts with the initial value(empty) before subsequent data is added to the array. As a result, the state ends up with only one item.
Below is some code for demo(I've simplified and omitted code for this demo):
// AccountProvider.tsx
const AccountProvider = ({children}: Props)=> {
const [favorites, setFavorites] = useState([])
const loadFavoriteItemForProduct = async (productId: string)=> {
const favoriteItem = await apiManager.getFavoriteItem(productId)
// favorites always resets to its inital value when this function is called
setFavorites([...favorites, favoriteItem])
}
const account: Account = {
loadFavoriteItemForProduct
}
return (
<AccountContext.Provider value={account}>
{children}
</AccountContext.Provider>
);
}
// ProductCard.tsx
// On mount, each productCard loads its favoriteItem data.
const ProductCard = ({product}: ProductCardProps) => {
const userAccount = useContext(AccountContext)
useEffect(()=>{
// loads product's favoritedItem. Favorites ought to contain
// as many favoriteItems in the backend. But, favorites is reinitialized before
// the new item is added. As a result, favorites contain only the favoriteItem of //the last ProductCard. See Screenshot attached
userAccount.loadFavoritedItemForProduct(product.productId)
}, [])
return (
<div>product.title</div>
)
}
// Products.tsx
const Products = () => {
const explore = useContext(ExploreContext)
return (
<div>
{explore.products.map(product => <ProductCard product={product} />)}
</div>
)
}
// index.tsx
...
<AccountProvider>
<ExploreProvider>
<Products />
</ExploreProvider>
</AccountProvider>
...
I'm just trying to use Context to model the userAccount which contains the users favorites. favorites ought to contain all favoriteItem data from the backend. But it only contains that of the last product. See attached screenshot.
Try passing an updater function to setFavorites.
setFavorites((favorites) => [...favorites, favoriteItem]);
React Docs:
https://beta.reactjs.org/apis/react/useState#updating-state-based-on-the-previous-state

Mapping through JSON data that contain additional status and results from messages

I'm trying to map through JSON data from React with this code...
import {useEffect,useState} from 'react'
function Tourapi() {
const[tours,settour]=useState([])
const[loading,setloading]=useState(true)
useEffect(()=>{
fetchtour()
},[])
const fetchtour= async()=> {
const furl='https://www.natours.dev/api/v1/tours'
const res = await fetch(furl)
const data = await res.json()
settour(data)
setloading(false)
}
if(!loading)
{
return (
<div>
{tours.map((tour)=>(
<h3> {tour.name}</h3>
))}
</div>
)
}
else
{
return <h3>LOADING.....</h3>
}
}
export default Tourapi
but it looks like that JSON data contain additional things and the data array so how can I Map through to get the name and description and so on...
the error that I got TypeError: tours.map is not a function
the JSON data begin with
{"status":"success","results":10,"data":{"data":[{"startLocation":{"type":"Point","coordinates":[-80.185942,25.774772],"description":"Miami, USA","address":"301 Biscayne Blvd, Miami, FL 33132, USA"}
what I mean how can I get out of ("status":"success","results":10,"data":) and get the data its self because it's not my API to control with the response
thanks.
This is what you need, I hope it's help
import { useState, useEffect } from "react";
function TourAPI() {
const [tours, settour] = useState([]);
const [loading, setloading] = useState(true);
useEffect(() => {
fetchtour();
}, []);
const fetchtour = async () => {
const furl = "https://www.natours.dev/api/v1/tours";
const res = await fetch(furl);
const data = await res.json();
await settour(data.data.data);
setloading(false);
};
if (!loading) {
return (
<>
<div className="tour-section">
{tours.map((tours, i) => {
return <h3 key={i}>{tours.name}</h3>;
})}
</div>
</>
);
} else {
return <h3>LOADING.....</h3>;
}
}
export default TourAPI;
I'll walk you through what's in the data, if you console.log(data) in your fetchtour, it'll contain an object that contains properties. It's like this
{status: 'success', results: 10, data: {data}}
So, you want to access one more data inside of that data
That'll lead to console.log(data.data) to see what's in there.
And it'll appear like this.
{data: Array(10)}
So, the data key actually contains the data's array inside of it, the data.data contains an array with 10 indexes in it as you can see. You can try in your browser to see that as well, and each index will contain the properties you want in there, which is name, and many more properties that you need to get like location, etc.
So lead us to the last data, if you console.log(data.data.data), it'll access to data that contains the 10 indexes, which it'll be an array [...] so that we can use map.
That's what you need to setState for the tours
And from that, we're good to use map, because we actually are able to map through an array that contains indexes.
And don't forget to set key for each div inside the map or you'll get the warning fire out from the console in your browser.
My English is not too well so my explanation might be a little bit hard but I hope you can understand what's going on.

react, map component, unexpected result

I am building Weather App, my idea is to save city name in database/localhost, place cities in useState(right now it's hard coded), iterate using map in first child component and display in second child component.
The problem is that 2nd child component outputs only one element (event though console.log prints both)
BTW when I change code in my editor and save, then another 'li' element appears
main component
const App = () => {
const [cities, setCities] = useState(['London', 'Berlin']);
return (
<div>
<DisplayWeather displayWeather={cities}/>
</div>
)
}
export default App
first child component
const DisplayWeather = ({displayWeather}) => {
const [fetchData, setFetchData] = useState([]);
const apiKey = '4c97ef52cb86a6fa1cff027ac4a37671';
useEffect(() => {
displayWeather.map(async city=>{
const res =await fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&units=metric&appid=${apiKey}`)
const data = await res.json();
setFetchData([...fetchData , data]);
})
}, [])
return (
<>
{fetchData.map(data=>(
<ul>
<Weather
data={data}/>
</ul>
))}
</>
)
}
export default DisplayWeather
second child component
const Weather = ({data}) => {
console.log(data) // it prints correctly both data
return (
<li>
{data.name} //display only one data
</li>
)
}
export default Weather
The Problem
The setFetchData hooks setter method is asynchronous by default, it doesn't give you the updated value of the state immediately after it is set.
When the weather result for the second city is returned and set to state, the current value fetchData at the time is still an empty array, so you're essentially spreading an empty array with the second weather result
Solution
Pass a callback to your setFetchData and get the current previous value of the state and then continue with your spread accordingly.
Like this 👇🏽
setFetchData((previousData) => [...previousData, data]);

How to map async key to call API

I have 2 classes, App.js and Categories.js. the App.js called an API and managed to get the name of the categories and the ID of the category into JSON, the data is then pushed like so and gets transfered into a prop of class Category
Object.keys(data.categories).forEach((category,index) => {
categories.push(data.categories[index].name);
categoryIDs.push(data.categories[index].id)
})
<Categories
categoryName = { this.state.categoryName }
categoryID = { this.state.categoryID }
/>
Then in my Categories class, the data is returning a pair through a map button, and i used key to associate with the array values like a pair but I can't pass the values to help get the values needed to dynamically access the API
class Categories extends React.Component {
getResult= async () => {
const api_call = await fetch(`
http://callthisapi.com/v1/categories/items?format=json&category=1334134&apiKey=${API_KEY}`)
// Convert response to json
const data = await api_call.json();
console.log(data);
}
render() {
return (
<div>
<br />
{
this.props.categoryName.map
((name,i) => <button key={i} onClick={this.getResult}> {name} {this.props.categoryID[i]} </button>)
}
</div>
as you can see the 1334134 is a hard coded value right now but the number is currently associated in {this.props.categoryID[i]}. How do i allow the number to change dynamically? I tried to pass in the value through a parameter but everything broke afterwards.
The value that I want is in {this.props.categoryID[i]} and I can't extract it to the function is the tl;dr
Change your onClick to be onClick={() => this.getResult(this.props.categoryID[i])}.
Then getResult can accept a parameter: getResult = async (id) => { ... }
API call: http://callthisapi.com/v1/categories/items?format=json&category=${id}
See React documentation for passing functions: https://reactjs.org/docs/faq-functions.html

React Redux, how to properly handle changing object in array?

I have a React Redux app which gets data from my server and displays that data.
I am displaying the data in my parent container with something like:
render(){
var dataList = this.props.data.map( (data)=> <CustomComponent key={data.id}> data.name </CustomComponent>)
return (
<div>
{dataList}
</div>
)
}
When I interact with my app, sometimes, I need to update a specific CustomComponent.
Since each CustomComponent has an id I send that to my server with some data about what the user chose. (ie it's a form)
The server responds with the updated object for that id.
And in my redux module, I iterate through my current data state and find the object whose id's
export function receiveNewData(id){
return (dispatch, getState) => {
const currentData = getState().data
for (var i=0; i < currentData.length; i++){
if (currentData[i] === id) {
const updatedDataObject = Object.assign({},currentData[i], {newParam:"blahBlah"})
allUpdatedData = [
...currentData.slice(0,i),
updatedDataObject,
...currentData.slice(i+1)
]
dispatch(updateData(allUpdatedData))
break
}
}
}
}
const updateData = createAction("UPDATE_DATA")
createAction comes from redux-actions which basically creates an object of {type, payload}. (It standardizes action creators)
Anyways, from this example you can see that each time I have a change I constantly iterate through my entire array to identify which object is changing.
This seems inefficient to me considering I already have the id of that object.
I'm wondering if there is a better way to handle this for React / Redux? Any suggestions?
Your action creator is doing too much. It's taking on work that belongs in the reducer. All your action creator need do is announce what to change, not how to change it. e.g.
export function updateData(id, data) {
return {
type: 'UPDATE_DATA',
id: id,
data: data
};
}
Now move all that logic into the reducer. e.g.
case 'UPDATE_DATA':
const index = state.items.findIndex((item) => item.id === action.id);
return Object.assign({}, state, {
items: [
...state.items.slice(0, index),
Object.assign({}, state.items[index], action.data),
...state.items.slice(index + 1)
]
});
If you're worried about the O(n) call of Array#findIndex, then consider re-indexing your data with normalizr (or something similar). However only do this if you're experiencing performance problems; it shouldn't be necessary with small data sets.
Why not using an object indexed by id? You'll then only have to access the property of your object using it.
const data = { 1: { id: 1, name: 'one' }, 2: { id: 2, name: 'two' } }
Then your render will look like this:
render () {
return (
<div>
{Object.keys(this.props.data).forEach(key => {
const data = this.props.data[key]
return <CustomComponent key={data.id}>{data.name}</CustomComponent>
})}
</div>
)
}
And your receive data action, I updated a bit:
export function receiveNewData (id) {
return (dispatch, getState) => {
const currentData = getState().data
dispatch(updateData({
...currentData,
[id]: {
...currentData[id],
{ newParam: 'blahBlah' }
}
}))
}
}
Though I agree with David that a lot of the action logic should be moved to your reducer handler.

Resources