Search box with React-Query - javascript

I am trying to implement Product search by text. Fetching data with react-query. The following implementation is working but it does not feel right to me. Let me know if I am overdoing it and if there is a simpler solution with react-query.
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useQueryClient } from 'react-query';
import ProductCard from '#/components/cards/ProductCard';
import { useQueryProducts } from '#/hooks/query/product';
import { selectSearch } from '#/store/search';
// function fetchProductsByFilter(text){}
const Shop = ({ count }) => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const { text } = useSelector(selectSearch);
const productsQuery = useQueryProducts(count);
useEffect(() => {
setProducts(productsQuery.data);
setLoading(false);
}, []);
const queryClient = useQueryClient();
useEffect(() => {
const delayed = setTimeout(() => {
queryClient.prefetchQuery(['searchProductsByText'], async () => {
if (text) {
const data = await fetchProductsByFilter(text);
setProducts(data);
setLoading(false);
return data;
}
});
}, 300);
return () => clearTimeout(delayed);
}, [text]);
return (
<div className="container-fluid">
<div className="row">
<div className="col-md-3">search/filter menu</div>
<div className="col-md-9">
{loading ? (
<h4 className="text-danger">Loading...</h4>
) : (
<h4 className="text-danger">Products</h4>
)}
{products.length < 1 && <p>No products found</p>}
<div className="row pb-5">
{products.map((item) => (
<div key={item._id} className="col-md-4 mt-3">
<ProductCard product={item} />
</div>
))}
</div>
</div>
</div>
</div>
);
};
// async function getServerSideProps(context) {}
export default Shop;

It doesn't seem very idiomatic to me. With react-query, the key to using filters are to put them into the query key. Since react-query refetches every time the key changes, you'll get a refetch every time you change a filter, which is usually what you want. It's a very declarative way of doing things. No useEffect needed at all.
If this happens when choosing something from a select or clicking an apply button, that's really all you need:
const [filter, setFilter] = React.useState(undefined)
const { data, isLoading } = useQuery(
['products', filter],
() => fetchProducts(filter)
{ enabled: Boolean(filter) }
)
Here, I am additionally disabling the query as long as the filter is undefined - fetching will start as soon as we call setFilter.
if typing into a text field is involved, I'd recommend some debouncing to avoid firing off too many requests. The useDebounce hook is very good for that. You'd still have the useState, but you'd use the debounced value for the query:
const [filter, setFilter] = React.useState(undefined)
const debouncedFilter = useDebounce(filter, 500);
const { data, isLoading } = useQuery(
['products', debouncedFilter],
() => fetchProducts(debouncedFilter)
{ enabled: Boolean(debouncedFilter) }
)

If this happens when choosing something from a select or clicking an apply button, that's really all you need:
const [filter, setFilter] = useState<string>('')
const isEnabled = Boolean(filter)
const { data, isLoading } = useQuery(
['products', filter],
() => fetchProducts(filter)
{ enabled: enabled: filter ? isEnabled : !isEnabled, }
)
<input type="text" onChange={(e) => setFilter(e.target.value)}/>

Related

make todo and store in localstorage but getting this error fetchdata.map is not a function at App (App.js:27:1)

import React, { useState, useEffect } from 'react';
import './style.css';
export default function App() {
const [state, setState] = useState([]);
const [inputData, setInputData] = useState();
const [fetchdata, setFetchData] = useState([])
const addHandler = () => {
setState((data) => {
return [...data, inputData];
});
localStorage.setItem('state', JSON.stringify(state));
setInputData('');
};
setFetchData(localStorage.getItem('state'))
return (
<div>
<input
onChange={(e) => setInputData(e.target.value)}
value={inputData || ''}
placeholder="add items"
/>
<button onClick={addHandler}>Add</button>
{fetchdata?.map((item) => {
return (
<div style={{ color: `#+${color}` }}>
<li key={item}>{item}</li>
</div>
);
}) || []}
</div>
);
}
This is the code I have tried also need dynamic colors for lists. Any help is appreciated with big thanks
even the key I have given unique but it says unique key required
Try to add a default value to your fetchData:
const [fetchdata, setFetchData] = useState(localStorage.getItem('state') ?? []);
and please don't begin to use useless useEffect like every begginer are doing, further documentation here !
try:
remove
setFetchData(localStorage.getItem('state'))
replace
const initData = () => {
try {
return JSON.parse(localStorage.getItem('state'));
} catch (e) {
return [];
}
}
const [fetchdata, setFetchData] = useState(initData())

How can I change the state of individual elements in a map function?

I want the content to display when the tab is clicked. The issue that I'm having is that once the tab is clicked, all the tabs open... and likewise close when clicked again. I've been trying for hours to figure out how to fix this. I thought I had an answer by having a state that I could set the index to and then write a condition for the tab to open when the index of the state is the same but I noticed that after clicking on another tab, the other one closes. I would appreciate it so much if someone could help me open an individual tab when it's clicked and always stay open until clicked again, meaning, I could have multiple tabs open at once.
Here's a demo:
https://codesandbox.io/s/orrigenda-react-question-5oxg47
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import LeaguesStyle from '../components/styles/LeaguesStyle.css';
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false)
const [isOpen, setOpen] = useState(false);
const getTeams = async () => {
try {
const res = await axios.get('https://api-football-standings.azharimm.site/leagues');
setTeams(res.data.data)
setLoading(true);
console.log(res.data)
} catch (err) {
alert(err.message)
}
}
useEffect(() => {
getTeams();
}, []);
return (
<div className="leagues">
{loading &&
teamz.map(item => (
<div className='teamCard' key={item.id}>
<div onClick={() => setOpen(!isOpen)} className="teamDiv">
<img src={item.logos.dark} className='teamLogo' />
<h1>{item.name}</h1>
</div>
{isOpen && <div className='card-content-active'>{item.abbr}</div>}
</div>
))}
</div>
);
}
You need to track the individual truthy values per item.id. This can be easily done by using an object to keep track of all the previous states via the spread operator. Once an initial state is set per tab, then it's just a matter of toggling that individual state between true and false. You delineate between tabs by dynamically assigning the id to the truthy value ([id]: !isOpen[id]). Here is the code in totality:
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [isOpen, setOpen] = useState({});
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const handleOpen = (id) => {
setOpen((prevTruthys) => ({ ...prevTruthys, [id]: !isOpen[id] }));
};
console.log(isOpen);
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => handleOpen(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isOpen[item.id] === true && (
<div className="card-content-active">{item.abbr}</div>
)}
</div>
))}
</div>
);
};
export default Leagues;
Here is the code sandbox: https://codesandbox.io/s/orrigenda-react-question-forked-42lbfo?file=/src/App.js
The solution is to store all clicked tabs in a list using the item ID, when the tab is open and you clicked again the ID is removed from the list
here is the code with the solution:
I created a function to update the state. setOpenById(tabId) and a function for checking if the tab is open isTabOpen(tabId)
the onClick now uses that function onClick={() => setOpenById(item.id)}
import React, { useEffect, useState } from "react";
import axios from "axios";
import LeaguesStyle from "./LeaguesStyle.css";
const Leagues = () => {
const [teamz, setTeams] = useState([]);
const [loading, setLoading] = useState(false);
const [openTab, setOpenTab] = useState([])
const getTeams = async () => {
try {
const res = await axios.get(
"https://api-football-standings.azharimm.site/leagues"
);
setTeams(res.data.data);
setLoading(true);
//console.log(res.data);
} catch (err) {
alert(err.message);
}
};
useEffect(() => {
getTeams();
}, []);
const setOpenById = (tabId) => {
if(!isTabOpen(tabId)){
setOpenTab([...openTab, tabId])
} else{
var array = [...openTab] // make a separate copy of the array
var index = array.indexOf(tabId)
if (index !== -1) {
array.splice(index, 1)
setOpenTab(array)
}
}
}
const isTabOpen = (tabId) => {
return openTab.indexOf(tabId) !== -1
}
return (
<div className="leagues">
{loading &&
teamz.map((item) => (
<div className="teamCard" key={item.id}>
<div onClick={() => setOpenById(item.id)} className="teamDiv">
<img src={item.logos.dark} className="teamLogo" alt="logo" />
<h1>{item.name}</h1>
</div>
{isTabOpen(item.id) && <div className="card-content-active">{item.abbr}</div>}
</div>
))}
</div>
);
};
export default Leagues;

useFetch custom hook not working properly

I am working on react website.
I have created one custom data fetching hook 'usePostFetch' as follows:
import React, { useState, useEffect } from "react";
//axios
import axios from "axios";
const usePostFetch = () => {
const [postData, setPostData] = useState([]);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const getData = async () => {
setIsLoading(true);
try {
const res = await axios.get("http://localhost:8000/Sell");
const data = await res.data;
setPostData(data);
setIsLoading(false);
} catch (error) {
console.log("Error from fetch: " + error);
setError(error.message);
setIsLoading(false);
}
};
getData();
}, []);
const values = [
...new Set(
postData.map((post) => {
return post.productType;
})
),
];
return { postData, values, error, isLoading };
};
export default usePostFetch;
I have a product page that renders when I click any of the links on the home page with a link "/product/:productId".productId is the id of clicked link product.
Product Page:
import React, { useEffect, useState } from "react";
//react router dom
import { useParams } from "react-router";
//Hooks
import usePostFetch from "../../Hooks/usePostFetch";
//styles
import { Wrapper, Info, Discription } from "./Product.styles";
//Server
const Server = "http://localhost:8000";
const Product = () => {
const { productId } = useParams();
const { postData, isLoading, error } = usePostFetch();
const [data, setData] = useState({});
console.log(postData, isLoading, error);
useEffect( () => {
const fetchData = async () => {
var value = await postData.filter((post) => {
return post._id === productId;
});
console.log(value);
setData(value);
};
fetchData();
}, [postData]);
return (
<Wrapper>
<Info>
{isLoading && <h1> Loading.... </h1>}
{error && <p>ERROR </p>}
{console.log(data)}
<img
src={`${Server}/productImages/${data[0].productImage}`}
alt={`${data[0].productName}`}
/>
<div className="data">
<h1>{data[0].productName}</h1>
<h3>{data[0].productPrice}</h3>
</div>
</Info>
</Wrapper>
);
};
export default Product;
But when I go to that link I got data in console like this:
Because of these empty arrays, I got errors like this:
What can I do or what is wrong with my code?
It appears you are reading state that doesn't exist yet. The initial data state is an empty object:
const [data, setData] = useState({});
And on the initial render you are attempting to read from a 0 property, which OFC is undefined still.
data[0] --> OK, undefined
data[0].productName --> NOT OK, throws error trying to access from undefined
You can conditionally render the data content when you know it's populated:
<Wrapper>
<Info>
{isLoading && <h1> Loading.... </h1>}
{error && <p>ERROR </p>}
{console.log(data)}
{data[0] && (
<img
src={`${Server}/productImages/${data[0].productImage}`}
alt={`${data[0].productName}`}
/>
<div className="data">
<h1>{data[0].productName}</h1>
<h3>{data[0].productPrice}</h3>
</div>
)
</Info>
</Wrapper>
Or you can just use the Optional Chaining operator to defend against null/undefined property accesses:
<Wrapper>
<Info>
{isLoading && <h1> Loading.... </h1>}
{error && <p>ERROR </p>}
{console.log(data)}
<img
src={`${Server}/productImages/${data[0]?.productImage}`}
alt={`${data[0]?.productName}`}
/>
<div className="data">
<h1>{data[0]?.productName}</h1>
<h3>{data[0]?.productPrice}</h3>
</div>
</Info>
</Wrapper>
It also seems that you are really expecting data to be an array, so you will want your initial state to maintain a state/type invariant, so it should also be declared as an array.
const [data, setData] = useState([]);

How to pass data from child to parent and render content based on selected value in dropdown?

I am learning React as I am fetching data from Pokéapi to make a list component, card component, detail component and filter component. I am trying to make a filter so you can filter by pokémon type. Only the cards that also contain that type string should then render (Not there yet). So I am not sure if a) I should make a different call from API inside PokemonList depending on selected value or b) if I should compare the values and just change how the PokemonCard element is rendered inside PokemonList.js depending on the comparison. I managed to pass data from filter to the list component. I have then been trying to pass the type data from PokemonCard.js to the list component so that I can compare these two values but I find it hard to use callbacks to pass the type data from the card component, since I dont pass it through an event or something like that.
Which method should I use here to simplify the filtering? Make different API call or render PokemonCard element conditionally?
Is it a good idea to compare filter option to pokemon card's type in PokemonList.js? Then how can I pass that data from the card component since I don't pass it through click event?
Thankful for any ideas! I paste the code from list component that contains the cards, card component and filter component.
PokemonList component:
import { useState } from 'react';
import useSWR from 'swr';
import PokemonCard from './PokemonCard';
import PokemonFilter from './PokemonFilter';
import './PokemonList.css';
const PokemonList = () => {
const [index, setIndex] = useState(0);
const [type, setType] = useState('');
function selectedType(type) { // value from filter dropdown
setType(type)
console.log("handled")
console.log(type)
}
const url = `https://pokeapi.co/api/v2/pokemon?limit=9&offset=${index}`;
const fetcher = (...args) => fetch(...args).then((res) => res.json())
const { data: result, error } = useSWR(url, fetcher);
if (error) return <div>failed to load</div>
if (!result) return <div>loading...</div>
result.results.sort((a, b) => a.name < b.name ? -1 : 1);
return (
<section>
<PokemonFilter onSelectedType={selectedType} selectedPokemonType={type} />
<div className="pokemon-list">
<div className="pokemons">
{result.results.map((pokemon) => (
<PokemonCard key={pokemon.name} pokemon={pokemon} /> // callback needed??
))}
</div>
<div className="pagination">
<button
onClick={() => setIndex(index - 9)}
disabled={result.previous === null}
>
Previous
</button>
<button
onClick={() => setIndex(index + 9)}
disabled={result.next === null}
>
Next
</button>
</div>
</div>
</section>
)
}
export default PokemonList;
PokemonCard component:
import { Link } from "react-router-dom";
import useSWR from 'swr';
import './PokemonCard.css';
const PokemonCard = ({ pokemon }) => {
const { name } = pokemon;
const url = `https://pokeapi.co/api/v2/pokemon/${name}`;
const { data, error } = useSWR(url);
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
const { types, abilities } = data;
// types[0].type.name <---- value I want to pass to PokemonList.js
return (
<div className='pokemon-card'>
<div className='pokemon-card__content'>
<img
className='pokemon-card__image'
src={data.sprites.front_default}
alt={name}
/>
<div className='pokemon-card__info'>
<p className='pokemon-card__name'>Name: {name}</p>
<p className='pokemon-card__abilities'>Abilities: {abilities[0].ability.name}</p>
<p className='pokemon-card__categories'>Category: {types[0].type.name}</p>
</div>
</div>
<Link className='pokemon-card__link' to={{
pathname: `/${name}`,
state: data
}}>
View Details
</Link>
</div>
)
}
export default PokemonCard;
PokemonFilter component:
import './PokemonFilter.css';
import useSWR from 'swr';
const PokemonFilter = ({onSelectedType, selectedPokemonType}) => {
const url = `https://pokeapi.co/api/v2/type/`;
const fetcher = (...args) => fetch(...args).then((res) => res.json())
const { data: result, error } = useSWR(url, fetcher);
if (error) return <div>failed to load</div>
if (!result) return <div>loading...</div>
function filteredTypeHandler(e) {
console.log(e.target.value);
onSelectedType(e.target.value);
}
console.log(selectedPokemonType)
return(
<div className="pokemon-types__sidebar">
<h2>Filter Pokémon by type</h2>
<select
name="pokemon-type"
className="pokemon-types__filter"
onChange={filteredTypeHandler}
>
<option value="All">Filter By Type</option>
{result.results.map((type) => {
return (
<option key={type.name} value={type.name}> {type.name}</option>
)
})}
</select>
</div>
)
}
export default PokemonFilter;
Here is an example to improve, modify, ... I didn't test, it's just a visual example.
I don't know about useSWR sorry, I use axios in my example...
If you want to centralize all your API requests, you can create a useApi hook, on the internet you will find tutorials.
PokemonList.js
import React, { useState, useEffect } from 'react';
import axios from 'axios'; // or swr
import PokemonFilter from './PokemonFilter';
import PokemonCard from './PokemonCard';
export default function PokemonList() {
const [data, setData] = useState([]);
const [filter, setFilter] = useState('');
// Executed every first render
useEffect(() => {
getData();
}, []);
// Executed only when filter changes
useEffect(() => {
getDataByTypes(filter);
}, [filter]);
// Get data
const getData = async () => {
const uri = 'https://xxx';
try {
const response = await axios.get(uri);
setData(response.data...);
} catch (error) {
console.log(error);
}
};
// Get data by types
const getDataByTypes = async (filter) => {
const uri = `https://xxx/type/${filter}...`;
if (filter) {
try {
const response = await axios.get(uri);
setData(response.data...);
} catch (error) {
console.log(error);
}
}
};
return (
<div className="main">
<PokemonFilter filter={filter} setFilter={setFilter} />
<div className="container">
<div className="cards-container">
{data.map((d) => (
<PokemonCard key={d.name} data={d} />
))}
</div>
</div>
</div>
);
}
PokemonCard.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function PokemonCard({ data }) {
const [pokemons, setPokemons] = useState();
useEffect(() => {
getPokemons(data);
}, [data]);
// Get Pokemons
const getPokemons = async (data) => {
const uri = `https://xxx/pokemon/${data.name}/`;
try {
const response = await axios.get(uri);
setPokemons(response.data...);
} catch (error) {
console.log(error);
}
};
return (
<div>
{pokemons && (
<div className="card">
<img src={pokemons.sprites.front_default} alt={pokemons.name} />
<p>{pokemons.name}</p>
<p>{pokemons.abilities[0].ability.name}</p>
<p>{pokemons.types[0].type.name}</p>
</div>
)}
</div>
);
}
PokemonFilter.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function PokemonFilter({ filter, setFilter }) {
const [types, setTypes] = useState([]);
useEffect(() => {
getType();
}, []);
// Get Type
const getType = async () => {
const uri = 'https://xxx/type/';
try {
const response = await axios.get(uri);
setTypes(response.data.results....);
} catch (error) {
console.log(error);
}
};
const handleFilter = (e) => {
setFilter(e.target.value);
};
return (
<select onChange={handleFilter} value={filter}>
<option>Filter by type</option>
{types.map((type) => {
return (
<option key={type.name} value={type.name}>
{type.name}
</option>
);
})}
</select>
);
}

Reactjs filter not returning correct users, when i delete the characters in the search filter

I am fetching the users from dummyapi and i am listing them. There is a search input, i want to filter the users on the page by name. When i type the characters, it filters correctly. When i start to delete the character, users are not listed correctly. It remains filtered. How can i fix this ? This is my code:
import { useEffect, useState } from "react";
import Header from "../components/Header";
import User from "./User";
import axios from "axios";
function App() {
const BASE_URL = "https://dummyapi.io/data/api";
const APP_ID = "your app id";
const [users, setUsers] = useState(null);
const handleChange = (e) => {
const keyword = e.target.value.toLowerCase();
const filteredUsers =
users &&
users.filter((user) => user.firstName.toLowerCase().includes(keyword));
setUsers(filteredUsers);
};
useEffect(() => {
async function fetchData() {
try {
const response = await axios.get(`${BASE_URL}/user?limit=1`, {
headers: { "app-id": APP_ID },
});
setUsers(response.data.data);
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
return (
<>
<Header />
<div className="container">
<div className="filter">
<h3 className="filter__title">USER LIST</h3>
<div>
<input
id="filter"
type="text"
placeholder="Search by name"
onChange={handleChange}
/>
</div>
</div>
<div className="user__grid">
{users &&
users.map((user, index) => {
const { id } = user;
return <User key={index} id={id} />;
})}
</div>
</div>
</>
);
}
export default App;
This is because you are manipulating the original array of users. So after each filter the original array has less values than previous hence after deleting it will search from the reduced number of elements.
To avoid this, keep original way as it is, apply filter on that and store the result in a separate array.
Something like this:
const [allUsers, setAllUsers] = useState(null); //will store original records
const [users, setUsers] = useState(null); // will store filtered results
then in useEffect hook:
useEffect(() => {
async function fetchData() {
try {
const response = await axios.get(`${BASE_URL}/user?limit=1`, {
headers: { "app-id": APP_ID },
});
setUsers(response.data.data);
setAllUsers(response.data.data); //add this line
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
and finally in handleChange event:
const handleChange = (e) => {
const keyword = e.target.value.toLowerCase();
// use allUsers array (with original unchanged data)
const filteredUsers =
allUsers &&
allUsers.filter((user) => user.firstName.toLowerCase().includes(keyword));
setUsers(filteredUsers);
};
Obviously, you can use some better approach, but this is just to give the idea of original issue.

Categories

Resources