I was implementing an Autocomplete feature while learning REACT by watching this Youtube Tutorial and here is the github repo of this project (incase you need to clone and run). I implemented it and it worked as expected. But there is a small functionality that it doesn't provide, i.e., I can't scroll down the Autocomplete list. Refer to this image for output. When I click the Side Scroll Bar The list Vanishes. How to activate that? Do I need to make another useEffect for Scroll Bar too?
Here is the App.js Code (Same as Github though)
import React, { useEffect, useState, useRef } from "react";
import logo from "./logo.svg";
import "./App.css";
const Auto = () => {
const [display, setDisplay] = useState(false);
const [options, setOptions] = useState([]);
const [search, setSearch] = useState("");
const wrapperRef = useRef(null);
useEffect(() => {
const pokemon = [];
const promises = new Array(20)
.fill()
.map((v, i) => fetch(`https://pokeapi.co/api/v2/pokemon-form/${i + 1}`));
Promise.all(promises).then(pokemonArr => {
return pokemonArr.map(value =>
value
.json()
.then(({ name, sprites: { front_default: sprite } }) =>
pokemon.push({ name, sprite })
)
);
});
setOptions(pokemon);
}, []);
useEffect(() => {
window.addEventListener("mousedown", handleClickOutside);
return () => {
window.removeEventListener("mousedown", handleClickOutside);
};
});
const handleClickOutside = event => {
const { current: wrap } = wrapperRef;
if (wrap && !wrap.contains(event.target)) {
setDisplay(false);
}
};
const updatePokeDex = poke => {
setSearch(poke);
setDisplay(false);
};
return (
<div ref={wrapperRef} className="flex-container flex-column pos-rel">
<input
id="auto"
onClick={() => setDisplay(!display)}
placeholder="Type to search"
value={search}
onChange={event => setSearch(event.target.value)}
/>
{display && (
<div className="autoContainer">
{options
.filter(({ name }) => name.indexOf(search.toLowerCase()) > -1)
.map((value, i) => {
return (
<div
onClick={() => updatePokeDex(value.name)}
className="option"
key={i}
tabIndex="0"
>
<span>{value.name}</span>
<img src={value.sprite} alt="pokemon" />
</div>
);
})}
</div>
)}
</div>
);
};
function App() {
return (
<div className="App">
<h1>Custom AutoComplete React</h1>
<div className="logo"></div>
<div className="auto-container">
<Auto />
</div>
</div>
);
}
export default App;
Please Help me to figure out this.
Sounds more like a CSS problem if I'm understnading it correctly. Try adding this to your ./App.css file:
.autoContainer{
max-height: 350px; overflow-y: auto; overflow-x: hidden;
}
Here is the idea: https://codesandbox.io/s/green-flower-zeued?file=/src/App.js:1509-1522
Related
I don't understand why my page can't recognize other pages when I click (for example on page 2, the same page appears again and again)
This is in MealNew.js component:
import React, {useEffect, useState } from "react";
import './MealNew.css';
import Card from "../UI/Card";
import AppPagination from "./AppPagination";
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query,setQuery] = useState('');
const[page,setPage] = useState(9);
const[numberOfPages,setNumberOfPages]= useState(10);
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=991fbfc719c743a5896bebbd98dfe996&page=${page}`;
fetch (link)
.then ((response)=> response.json())
.then ((data) => {
setData(data.results)
setNumberOfPages(data.total_pages)
const elementFood = data?.map((meal,key) => {
return (<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image}
alt='e-meal'/>
</div> )
})
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
}
useEffect(()=> {
handleClick();
},[page])
return (
<Card className="meal">
<form onSubmit={handleSubmit}>
<input
className="search"
placeholder="Search..."
value={query}
onChange={(e)=>setQuery(e.target.value)}/>
<input type='submit' value='Search'/>
</form>
<li className="meal">
<div className = 'meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination
setPage={setPage}
pageNumber={numberOfPages}
/>
</div>
</li>
</Card>
) }
export default MealNew;
This is in AppPagination.js component:
import React from "react";
import { Pagination } from "#mui/material";
const AppPagination = ({setPage,pageNumber}) => {
const handleChange = (page)=> {
setPage(page)
window.scroll(0,0)
console.log (page)
}
return (
<div >
<div >
<Pagination
onChange={(e)=>handleChange(e.target.textContent)}
variant="outlined"
count={pageNumber}/>
</div>
</div>
)
}
export default AppPagination;
Thanks in advance, I would appreciate it a lot
The only error I am getting in Console is this:
Line 64:3: React Hook useEffect has a missing dependency: 'handleClick'. Either include it or remove the dependency array react-hooks/exhaustive-deps
You are not following the spoonacular api.
Your link looks like this:
https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&page=${page}
I checked the spoonacular Search Recipes Api and there's no page parameter you can pass. You have to used number instead of page.
When you receive response from the api, it returns the following keys: offset, number, results and totalResults.
You are storing totalResults as totalNumberOfPages in state which is wrong. MUI Pagination count takes total number of pages not the total number of records. You can calculate the total number of pages by:
Math.ceil(totalRecords / recordsPerPage). Let say you want to display 10 records per page and you have total 105 records.
Total No. of Pages = Math.ceil(105/10)= 11
Also i pass page as prop to AppPagination component to make it as controlled component.
Follow the documentation:
Search Recipes
Pagination API
Complete Code
import { useEffect, useState } from "react";
import { Card, Pagination } from "#mui/material";
const RECORDS_PER_PAGE = 10;
const MealNew = () => {
const [data, setData] = useState([]);
const [showData, setShowData] = useState(false);
const [query, setQuery] = useState("");
const [page, setPage] = useState(1);
const [numberOfPages, setNumberOfPages] = useState();
const handleClick = () => {
setShowData(true);
const link = `https://api.spoonacular.com/recipes/complexSearch?query=${query}&apiKey=<API_KEY>&number=${page}`;
fetch(link)
.then((response) => response.json())
.then((data) => {
setData(data.results);
const totalPages = Math.ceil(data.totalResults / RECORDS_PER_PAGE);
setNumberOfPages(totalPages);
});
};
const elementFood = data?.map((meal, key) => {
return (
<div key={key}>
<h1>{meal.title}</h1>
<img src={meal.image} alt='e-meal' />
</div>
);
});
const handleSubmit = (e) => {
e.preventDefault();
handleClick();
};
useEffect(() => {
handleClick();
console.log("first");
}, [page]);
return (
<Card className='meal'>
<form onSubmit={handleSubmit}>
<input className='search' placeholder='Search...' value={query} onChange={(e) => setQuery(e.target.value)} />
<input type='submit' value='Search' />
</form>
<li className='meal'>
<div className='meal-text'>
<h5>{showData && elementFood}</h5>
<AppPagination setPage={setPage} pageNumber={numberOfPages} page={page} />
</div>
</li>
</Card>
);
};
const AppPagination = ({ setPage, pageNumber, page }) => {
const handleChange = (page) => {
setPage(page);
window.scroll(0, 0);
console.log(page);
};
console.log("numberOfPages", pageNumber);
return (
<div>
<div>
<Pagination
page={page}
onChange={(e) => handleChange(e.target.textContent)}
variant='outlined'
count={pageNumber}
/>
</div>
</div>
);
};
export default MealNew;
I am currently trying to map through every planet in the star wars api and print out the individual planet name but I am stuck. API LINK (https://swapi.dev/)
Thankyou for any help or advice
I tried using axios and Use Effect but am not sure where my errors may be. Below is what i have in my AllPlanets React Component.
`
import React from "react";
import axios from "axios";
import { useState, useEffect } from "react";
const AllPlanets = () => {
const [data, setData] = useState("");
useEffect(() => {
axios
.get(`https://swapi.dev/api/planets`)
.then((res) => setData(res.data))
.catch((error) => console.log(error));
});
return (
<div>
{data && (
<div className="flex">
{data.map((planet, idx) => (
<p>{planet.name}</p>
))}
</div>
)}
</div>
);
};
export default AllPlanets;
`
These are the needed fixes (see comments in code as well):
The API response contains the property results that has the actual list of planets
The useEffect needs an empty dependencies array to block it from running on each render, and creating an infinite loop
Each item in an rendered list needs a key with a unique value
const { useState, useEffect } = React;
const AllPlanets = () => {
const [data, setData] = useState([]);
useEffect(() => {
axios
.get(`https://swapi.dev/api/planets`)
.then(res => setData(res.data.results)) // get the results property from the res.data object
.catch(error => console.log(error));
}, []); // add a dependency array so you won't have an infinite loop
return (
<div>
{!!data.length && (
<div className="flex">
{data.map(({ name }) => (
// use the name as key
<p key={name}>{name}</p>
))}
</div>
)}
</div>
);
};
ReactDOM
.createRoot(root)
.render(<AllPlanets />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.1.3/axios.min.js" integrity="sha512-0qU9M9jfqPw6FKkPafM3gy2CBAvUWnYVOfNPDYKVuRTel1PrciTj+a9P3loJB+j0QmN2Y0JYQmkBBS8W+mbezg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<div id="root"></div>
If you console.log the result you'll notice it's not an array of planets. It's an object with this structure:
{
count: 60
next: "https://swapi.dev/api/planets/?page=2"
previous: null
results: Array(10)
}
So replace
.then((res) => setData(res.data))
with
.then((res) => setData(res.data.results))
Alternatively, if you want to use next and previous provided links, for pagination, keep data in the returned format and map its results instead:
{data.results.map((planet, idx) => (
<p>{planet.name}</p>
))}
... and also make use of the previous and next links.
Here's an example:
const { useState, useEffect, useCallback, Fragment } = React;
const Button = ({ label, ...args }) => <button {...args}>{label}</button>;
const App = () => {
const [data, setData] = useState(null);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const load = useCallback((url) => {
setLoading(true);
axios
.get(url)
.then(({ data }) => {
setData(data);
setPage(Number(url[url.length - 1]) || 1);
setLoading(false);
})
.catch(console.log);
}, []);
useEffect(() => {
load("https://swapi.dev/api/planets");
}, []);
const mapButton = useCallback(
(type) => ({
label: type === "next" ? "→" : "←",
onClick: () => load(data[type]),
disabled: !data[type],
}),
[data]
);
return loading ? (
<div>Loading...</div>
) : (
(data && (
<Fragment>
<Button {...mapButton("previous")} />
{data.results.length && (
<small>
Page {page} of {Math.ceil(data.count / data.results.length)}
</small>
)}
<Button {...mapButton("next")} />
<div className="flex">
{data.results.map((planet, key) => (
<p {...{ key }}>{planet.name}</p>
))}
</div>
</Fragment>
)) ||
null
);
};
ReactDOM.createRoot(root).render(<App />);
#root {
font-family: sans-serif;
text-align: center
}
button:not(:disabled) {
cursor: pointer
}
small {
padding: 0 1rem
}
<script src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/axios#1.1.3/dist/axios.min.js"></script>
<div id="root"></div>
I have React component:
Main.jsx
import { useState, useEffect } from "react";
import { Preloader } from "../Preloader";
import { Pokemons } from "../Pokemons";
import { LoadMore } from "../LoadMore";
function Main() {
const [pokemons, setPokemons] = useState([]);
const [loading, setLoading] = useState(true);
const [pokemonsPerPage] = useState(20);
const [page, setPage] = useState(1);
function getPokemons(pokemonOffset) {
fetch(
`https://pokeapi.co/api/v2/pokemon?limit=${pokemonsPerPage}&offset=${pokemonOffset}`
)
.then((responce) => responce.json())
.then((data) => {
data.results && setPokemons((p) => [...p, ...data.results]);
setLoading(false);
});
}
useEffect(() => {
const offset = page * pokemonsPerPage - pokemonsPerPage;
getPokemons(offset);
}, [page]);
return (
<main className="container content">
{loading ? <Preloader /> : <Pokemons pokemons={pokemons} />}
<LoadMore next={() => setPage((p) => p + 1)} />
</main>
);
}
export { Main };
Pokemon.jsx
import { useState, useEffect } from "react";
function Pokemon({ name, url }) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url)
.then((r) => r.json())
.then(setData);
}, [url]);
return (
<div>
{data ? (
<div className="card animate__animated animate__fadeIn">
<div className="card-image">
<img src={data.sprites.front_default} />
<span className="card-title">{name}</span>
</div>
<div className="card-content">
{data.abilities.map((n, index) => (
<p key={index}>{n.ability.name}</p>
))}
</div>
</div>
) : (
<div>loading...</div>
)}
</div>
);
}
export { Pokemon };
I need each card (Pokemon) to have a "Details" button, which, when clicked, displays additional (unique) information from the fetch request in the "url" for the selected card
I think I need to do this in Pokemon.jsx but I just started learning React and haven't come across a similar challenge
If you just need a button for each card I would assume this
{data.map((item, index) =>
<div key={index}>
....
<button onClick={()=> { do something }}>
</div>
)}
and then create a function that fetches data and add it to your array where you keep
the data and might have to mess with the useEffect when you want to see the change.
I've almost completed my small project, which is a react app which calls on a API that displays dog images. However, at the moment, the show and hide buttons are targeting all of the images on the page (12 in total). I simply want to target one image at a time, so when I click hide or show, it will only do so for one of the images.
App.js
import './App.css';
import './Dog.js';
import './index.css';
import FetchAPI from './FetchAPI';
function DogApp() {
return (
<div className="dogApp">
<FetchAPI />
</div>
);
}
export default DogApp;
FetchAPI.js
import React, { useState, useEffect } from 'react'
// hide the div then display it with onclick
const FetchAPI = () => {
const [show, setShow] = useState(false);
const [data, setData] = useState([]);
const apiGet = () => {
const API_KEY = "";
fetch(`https://api.thedogapi.com/v1/images/search?limit=12&page=10&order=Desc?API_KEY=${API_KEY}`)
.then((response) => response.json())
.then((json) => {
console.log(json);
setData([...data, ...json]);
});
};
useEffect(() => { //call data when pagee refreshes/initially loads
apiGet();
}, []);
return (
<div>
{data.map((item) => (
<div class="dog">
<img alt ="dog photos" class="flexMate" src={item.url}></img>
{show ? <p>{JSON.stringify(item.breeds)}</p> : null}
<button onClick={() => setShow(true)}>Show</button>
<button onClick={() => setShow(false)}>Hide</button>
</div>
))}
</div>
)
}
export default FetchAPI;
import React, { useState, useEffect } from 'react'
// hide the div then display it with onclick
const FetchAPI = () => {
const [show, setShow] = useState({});
const [data, setData] = useState([]);
const apiGet = () => {
const API_KEY = "";
fetch(`https://api.thedogapi.com/v1/images/search?limit=12&page=10&order=Desc?API_KEY=${API_KEY}`)
.then((response) => response.json())
.then((json) => {
console.log(json);
setData([...data, ...json]);
});
};
useEffect(() => { //call data when pagee refreshes/initially loads
apiGet();
}, []);
return (
<div>
{data.map((item, id) => (
<div class="dog">
<img alt ="dog photos" class="flexMate" src={item.url}></img>
{show[id] !== false ? <p>{JSON.stringify(item.breeds)}</p> : null}
<button onClick={() => setShow((prev) => ({ ...prev, [id]: true }))}>Show</button>
<button onClick={() => setShow((prev) => ({ ...prev, [id]: false }))}>Hide</button>
</div>
))}
</div>
)
}
export default FetchAPI;
I'm trying to use a search bar component in my React project to search/filter through an api list of movies by title. Right now my search term is console logging, but i'm trying to filter the movie list to only show the titles that match the term. I'm having issues with updating my movies state with the term and displaying the new array.
App
import SearchBar from "../Search/SearchBar"
export default function Movies() {
const [movies, setMovies] = useState([]);
async function getMovies() {
const movieData = await fetchMovies();
console.log(movieData);
setMovies(
movieData.data.data.sort((a, b) => a.title.localeCompare(b.title))
);
}
useEffect(() => {
getMovies();
}, []);
async function onSearchSubmit(term) {
console.log(term)
let fill = []
movies.filter((movie) => {
if(movie.title === term) {
fill.push(movie.title)
}
setMovies(fill)
})
}
return (
<>
<Nav
movies={movies}
setMovies={setMovies}/>
<SearchBar
onSubmit={onSearchSubmit}/>
{movies ? (
<div>
<div>
{movies.map((m, idx) => {
return <div key={idx}>{m.title}</div>;
})}{" "}
</div>
</div>
) : (
"loading..."
)}
</>
);
}
Search Bar component
import React,{useState} from 'react';
const SearchBar = ({onSubmit}) => {
const [term, setTerm] = useState("")
function onFormSubmit(event){
event.preventDefault()
onSubmit(term)
}
return (
<div className="ui segment">
<form onSubmit={onFormSubmit} className="ui form">
<div className="field">
<label>Movie Search</label>
<input
type="text"
value={term}
onChange={(e) => setTerm( e.target.value)}
/>
</div>
</form>
</div>
);
}
export default SearchBar;
First of all additional state is needed to record the loaded moves list:
const movies = useRef([]);
const [filteredMovies, setFilteredMovies] = useState([]);
It is better to declare handlers with useCallback and avoid the mixture of declarative and imperative styles. For example:
const onSearchSubmit = useCallback(async (term) => {
if (term) {
const _ = movies.current.filter(({ title }) => (title === term));
setFilteredMovies(_);
} else {
setFilteredMovies(movies.current);
}
}, [movies]);
https://jsfiddle.net/pq9xkewz/2/