I am doing a small project and have a list of components that display information about countries. Now I have added react router so that when I click on a card it displays more information about that country. Now when I click on the card nothing happens! Below is the code for the Countries.
import React, { Component } from 'react';
import { CountryList } from './Components/Card-List/CountryList';
import { SearchBox } from './Components/Search-box/Search-Box';
import './Countries.styles.css';
import { DetailCountryCard } from './Components/DetailCountryCard/DetailCountryCard';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
class Countries extends Component {
constructor() {
super();
this.state = {
countries:[],
searchField:"",
regionField:"",
darkMode: false
}
this.setDarkMode = this.setDarkMode.bind(this);
};
componentDidMount() {
fetch("https://restcountries.eu/rest/v2/all")
.then(response => response.json())
.then(all => this.setState({ countries: all,
regions: all}))
.catch(error => console.log("I have errored" + error));
}
setDarkMode(e){
this.setState((prevState) => ({ darkMode: !prevState.darkMode }));
}
render() {
const { countries, searchField, regionField, darkMode } = this.state;
const filterCountries = countries.filter((country) => country.name.toLowerCase().includes(searchField.toLowerCase()) &&
country.region.toLowerCase().includes(regionField.toLowerCase()));
return(
<Router>
<div className={darkMode ? "dark-mode" : "light-mode" }>
<nav className="navbar-items">
<h1 className="header">Where in the World</h1>
<div className="moon-end">
<button onClick={this.setDarkMode}>
<i className={darkMode ? "moon fas fa-moon" : "moon far fa-moon" }></i>
</button>
<h2>{darkMode ? "Dark Mode" : "Light Mode" }</h2>
</div>
</nav>
<div className="Input">
< SearchBox type="search" placeholder="Search a Country" handlechange={e=> this.setState({
searchField: e.target.value })}
/>
< SearchBox type="regions" placeholder="Filter by Regions" handlechange={e=> this.setState({
regionField: e.target.value })}
/>
</div>
<CountryList countries={filterCountries} />
{/* <Route path="/" exact component={Countries} /> */}
<Switch>
<Route path="/card-detail/:name" component={ DetailCountryCard } exact/>
</Switch>
</div>
</Router>
);
}
}
export default Countries
The link for each card is in the following component:
import React from 'react';
import './CountryList.styles.css';
import {Link} from 'react-router-dom'
import { CountryCard } from '../Card/CountryCard';
export const CountryList = (props) => (
<div className='card-list'>
{props.countries.map(country => (
<Link to={`/card-detail/${country.name}`} >
<CountryCard key={country.alpha2Code} country={country} />
</Link>
))}
</div>
);
This should go to the following component:
import React from 'react';
import { useEffect } from 'react';
import { useState } from 'react';
export const DetailCountryCard = ({match}) => {
useEffect(() => {
fetchItem();
console.log(match);
},[])
const [country, setCountry] = useState([])
const fetchItem = async ()=> {
const fetchCountry = await fetch(`https://restcountries.eu/rest/v2/name/${match.params.name}`);
const countries = await fetchCountry.json();
setCountry(countries);
console.log(country);
}
return (
<div>
{country.map(town => (
<div>
<h1 key={town.alpha2Code}>{town.name}</h1>
<p>Native Name{town.nativeName}</p>
<p>Region: {town.region}</p>
<p>Languages: {town.languages[0].name}</p>
</div>
))}
</div>
);
}
Not sure what I am missing. I don't think I have done a typo on the component. So not sure why it is not rendering? Any help would be appreciated.
You just need add dependency of match in useEffect in DetailCountryCard. Because [] its similar in Class ComponentcomponentDidMount()` and you need to listen when match it's changed.
This is final code to DetailCountryCard:
import React from "react";
import { useEffect } from "react";
import { useState } from "react";
export const DetailCountryCard = ({ match }) => {
useEffect(() => {
fetchItem();
console.log(match);
}, [match]);
const [country, setCountry] = useState([]);
const fetchItem = async () => {
const fetchCountry = await fetch(
`https://restcountries.eu/rest/v2/name/${match.params.name}`
);
const countries = await fetchCountry.json();
setCountry(countries);
console.log(country);
};
return (
<div>
{country.map(town => (
<div>
<h1 key={town.alpha2Code}>{town.name}</h1>
<p>Native Name{town.nativeName}</p>
<p>Region: {town.region}</p>
<p>Languages: {town.languages[0].name}</p>
</div>
))}
</div>
);
};
I tested in CodeSandBox and it works!
Link
Related
I am using the Google Books API to retrieve book details in my application. I have two routes: one with a form and the search results, and the other with the full details of each search result. Currently,
I have a link on the details page that takes me back to the form page, but I want to retain the state of the form page and display the search results when I click the link instead of just the form. How can I do this?
You can check my code below.
BookDetails.jsx
import { useParams } from "react-router-dom";
import fetchBookDetails from "../hooks/fetchBookDetails";
import { useQuery } from "#tanstack/react-query";
import { Link } from "react-router-dom";
import notFoundImage from "../assets/react.svg";
import Header from "./Header";
const BookDetails = () => {
const { id } = useParams();
const results = useQuery(["book", id], fetchBookDetails);
if (results.isLoading) {
return <div>Loading...</div>;
}
if (results.isError) {
return <div>Book with id {id} not found.</div>;
}
if (results.data) {
return (
<div>
<Header />
<Link to="/">
<button className="back">Back</button>
</Link>
<div className="book-details">
<img
src={
results.data.volumeInfo.imageLinks
? results.data.volumeInfo.imageLinks.thumbnail
: notFoundImage
}
alt={results.data.volumeInfo.title}
/>
<h1>{results.data.volumeInfo.title}</h1>
<p>{results.data.volumeInfo.authors}</p>
</div>
</div>
);
}
};
export default BookDetails;
SearchForm.jsx
const SearchForm = ({ onSubmit }) => {
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(e.target.elements.book.value);
};
return (
<>
{" "}
<div>
<form onSubmit={handleSubmit}>
<label htmlFor="book">
Book Name:
<input type="text" name="book" />
</label>
<button type="submit">Submit</button>
</form>
</div>
</>
);
};
export default SearchForm;
Seachresult.jsx
import React from "react";
import { Link } from "react-router-dom";
import notFoundImage from "../assets/react.svg";
const SearchResults = ({ results }) => {
return results.isLoading ? (
<div>Loading...</div>
) : results.isError ? (
<div>{results.error.message}</div>
) : (
<div className="books">
{results.data.items.map((item, i) => {
return (
<Link to={`/book/${item.id}`} key={item.id}>
<div key={item.id} className="book border-2-red-500">
<p>{i + 1}</p>
<img
src={
item.volumeInfo.imageLinks
? item.volumeInfo.imageLinks.thumbnail
: notFoundImage
}
alt={item.volumeInfo.title}
/>
<h3>{item.volumeInfo.title}</h3>
<p>{item.volumeInfo.authors}</p>
</div>
</Link>
);
})}
</div>
);
};
export default SearchResults;
Home.jsx
import { useState } from "react";
import { useQuery } from "#tanstack/react-query";
import fetchBooks from "../hooks/fetchBooks";
import SearchResults from "./SearchResults";
import SearchForm from "./SearchForm";
import Header from "./Header";
const Home = () => {
const [book, setBook] = useState(null);
const results = useQuery(["search", book], fetchBooks);
return (
<main>
<Header />
<SearchForm onSubmit={setBook} />
<SearchResults results={results} />
</main>
);
};
export default Home;
And lastly, App.jsx
import "./App.css";
import { QueryClient, QueryClientProvider } from "#tanstack/react-query";
import Home from "./components/Home";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import BookDetails from "./components/BookDetails";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
cacheTime: Infinity,
},
},
});
function App() {
return (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<Routes>
<Route path="/book/:id" element={<BookDetails />} />
<Route path="/" element={<Home />} />
</Routes>
</QueryClientProvider>
</BrowserRouter>
);
}
export default App;
You can use React Context API, create a Context that wraps the components you want to grant access to that states, and then you can manage the state in a "global" way in order to keep it intact when going back and forth.
You can check the example bellow:
import React, { useState, createContext, useCallback, useContext } from 'react';
import { uuid } from 'uuidv4';
import ToastContainer from '../components/ToastContainer';
interface ToastContextData {
addToast(messages: Omit<ToastMessage, 'id'>): void;
}
export interface ToastMessage {
id: string;
type?: 'success' | 'error' | 'info';
title: string;
description: string;
}
const ToastContext = createContext<ToastContextData>({} as ToastContextData);
const ToastProvider: React.FC = ({ children }) => {
const [messages, setMessages] = useState<ToastMessage[]>([]);
const addToast = useCallback(
({ type, title, description }: Omit<ToastMessage, 'id'>) => {
const id = uuid();
const toast = {
id,
type,
title,
description,
};
setMessages((state) => [...state, toast]);
},
[]
);
}, []);
return (
<ToastContext.Provider value={{ addToast }}>
{children}
<ToastContainer messages={messages} />
</ToastContext.Provider>
);
};
function useToast(): ToastContextData {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
}
export { ToastProvider, useToast };
I have been trying to set a search filter form. I am getting data from API (an array of cake objects with "id", "cake_name", "category" etc properties), these get displayed properly. But somehow my search function is not working? It should allow the user to input a name of a cake which then would be filtered through the cakes available and only the searched one(s) would be displayed.
I am getting this error:
error
Here is my code:
context.js:
import React, { useState, useContext, useEffect } from "react";
import { useCallback } from "react";
const url = "https://cakeaddicts-api.herokuapp.com/cakes";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [cakes, setCakes] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const fetchCakes = async () => {
setLoading(true);
try {
const response = await fetch(url);
const cakes = await response.json();
if (cakes) {
const newCakes = cakes.map((cake) => {
const {
id,
image,
cake_name,
category,
type,
ingredients,
instructions,
} = cake;
return {
id,
image,
cake_name,
category,
type,
ingredients,
instructions,
};
});
setCakes(newCakes);
console.log(newCakes);
} else {
setCakes([]);
}
setLoading(false);
} catch (error) {
console.log(error);
setLoading(false);
}
};
useEffect(() => {
fetchCakes();
}, []);
return (
<AppContext.Provider
value={{
loading,
cakes,
setSearchTerm,
searchTerm,
filteredData,
setFilteredData,
}}
>
{children}
</AppContext.Provider>
);
};
// make sure use
export const useGlobalContext = () => {
return useContext(AppContext);
};
export { AppContext, AppProvider };
SearchForm.js
import React from "react";
import { useGlobalContext } from "../context";
import CakeList from "./CakeList";
const SearchForm = () => {
const { cakes, setSearchTerm, searchTerm, setFilteredData } =
useGlobalContext;
const searchCakes = () => {
if (searchTerm !== "") {
const filteredData = cakes.filter((item) => {
return Object.values(item)
.join("")
.toLowerCase()
.includes(searchTerm.toLowerCase());
});
setFilteredData(filteredData);
} else {
setFilteredData(cakes);
}
};
return (
<section className="section search">
<form className="search-form">
<div className="form-control">
<label htmlFor="name">Search Your Favourite Cake</label>
<input
type="text"
id="name"
onChange={(e) => searchCakes(e.target.value)}
/>
</div>
</form>
</section>
);
};
export default SearchForm;
CakeList.js:
import React from "react";
import Cake from "./Cake";
import Loading from "./Loading";
import { useGlobalContext } from "../context.js";
const CakeList = () => {
const { cakes, loading, searchTerm, filteredResults } = useGlobalContext();
if (loading) {
return <Loading />;
}
return (
<section className="section">
<h2 className="section-title">Cakes</h2>
<div className="cakes-center">
{searchTerm.length > 1
? filteredResults.map((cake) => {
return <Cake key={cake.id} {...cake} />;
})
: cakes.map((item) => {
return <Cake key={item.id} {...item} />;
})}
</div>
</section>
);
};
export default CakeList;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
// import pages
import Home from "./pages/Home";
import About from "./pages/About";
import SingleCake from "./pages/SingleCake";
import Error from "./pages/Error";
// import components
import Navbar from "./components/Navbar";
function App() {
return (
<Router>
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/cake/:id" element={<SingleCake />} />
<Route path="*" element={<Error />} />
</Routes>
</Router>
);
}
export default App;
Can someone please help me with this search form? I have tried so many things and nothing is working :( Anyone?
On line 11 of SearchForm.js, there is a part that reads cakes.filter(. To resolve the typeError, change this to cakes?.filter(. This will only execute the filter if cakes is defined. It's a feature in javascript called Optional Chaining, introduced in ES2020.
Learn about it more here
I am rebuilding a static website in react https://movie-list-website-wt.netlify.app/ I am trying to transfer the search function to react. My current search function works as intended, it returns an array of movies that is being searched, I want it to update the data fetched by the movie cards so that when I search a query, the movie cards use the returned search data instead of the original one
this is my App.js file
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Movies from './components/Movies';
import Topnav from './components/Topnav';
import './App.css';
import './components/Movies.css';
export const API_KEY = 'api_key=247b6aa143f3f2c0b100c0cbdfb1ac99';
export const BASE_URL = 'https://api.themoviedb.org/3';
export const API_URL = BASE_URL + '/discover/movie?sort_by=popularity.desc&' + API_KEY;
export const IMG_URL = 'https://image.tmdb.org/t/p/w500';
export const searchURL = BASE_URL + '/search/movie?'+ API_KEY;
function App() {
const [movies, setMovies] = useState ([]);
useEffect(() => {
fetch (API_URL)
.then(res => res.json())
.then(data => {
console.log(data);
setMovies(data.results);
});
}, [])
return (
<>
<Router>
<Topnav />
<div className="movie-container">
{movies.length > 0 &&
movies.map((movie) => (
<Movies key={movie.id} {...movie} />))}
</div>
<Routes>
<Route exact path='/' />
</Routes>
</Router>
</>
);
}
export default App;
and this is part of my Top navigation component that includes the search bar and function
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import './Topnav.css';
import { searchURL } from '../App';
function Topnav() {
const [click, setClick] = useState(false)
const handleClick = () => setClick(!click)
const modeToggle = () => {
document.body.classList.toggle('dark')
}
const [query, setQuery] = useState("")
const onChange = (e) => {
e.preventDefault();
setQuery(e.target.value)
fetch(searchURL+`&language=en-US&page=1&include_adult=false&query=${e.target.value}`)
.then((res) => res.json())
.then((data) => {
console.log(data.results);
})
}
return (
<>
<nav className="topnav">
<div className="topnav-container">
<Link to='/' className='topnav-logo'>
<img src={require('../img/logo.png').default} alt="logo" />
</Link>
<div className="wrapper">
<form id='search-bar'>
<input
type="text"
placeholder="Search"
className="search"
id="search"
value={query}
onChange={onChange}
/>
</form>
<label className="switch">
<input type="checkbox" id="mode-toggle" onChange={modeToggle}/>
</label>
</div>
If you need to filter the movies array in the Topnav component you can just pass the setMovies state function as prop to the component.
All you need to do is update the data in the onChange method:
App.js
return (
<>
<Router>
<!-- Pass the prop to the component -->
<Topnav setMovies={setMovies}/>
<div className="movie-container">
{movies.length > 0 &&
movies.map((movie) => (
<Movies key={movie.id} {...movie} />))}
</div>
<Routes>
<Route exact path='/' />
</Routes>
</Router>
</>
);
Topnav.js
function Topnav({ setMovies }) {
...
const onChange = (e) => {
e.preventDefault();
setQuery(e.target.value)
fetch(searchURL+`&language=en-US&page=1&include_adult=false&query=${e.target.value}`)
.then((res) => res.json())
.then((data) => {
console.log(data.results);
// Update the data once fetched
setMovies(data.results)
})
}
...
It is strait forward just move the method onChange to the parent component and pass it as a props
import React, { useEffect, useState } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Movies from './components/Movies';
import Topnav from './components/Topnav';
import './App.css';
import './components/Movies.css';
export const API_KEY = 'api_key=247b6aa143f3f2c0b100c0cbdfb1ac99';
export const BASE_URL = 'https://api.themoviedb.org/3';
export const API_URL = BASE_URL + '/discover/movie?sort_by=popularity.desc&' + API_KEY;
export const IMG_URL = 'https://image.tmdb.org/t/p/w500';
export const searchURL = BASE_URL + '/search/movie?'+ API_KEY;
function App() {
const [movies, setMovies] = useState ([]);
const [, setQuery] = useState("")
useEffect(() => {
fetch (API_URL)
.then(res => res.json())
.then(data => {
console.log(data);
setMovies(data.results);
});
}, [])
const onChange = (e) => {
e.preventDefault();
setQuery(e.target.value)
fetch(searchURL+`&language=en-US&page=1&include_adult=false&query=${e.target.value}`)
.then((res) => res.json())
.then((data) => {
console.log(data.results);
// DO what ever you want with the move array
})
}
return (
<>
<Router>
<Topnav onChange={onChange} query={query}/>
<div className="movie-container">
{movies.length > 0 &&
movies.map((movie) => (
<Movies key={movie.id} {...movie} />))}
</div>
<Routes>
<Route exact path='/' />
</Routes>
</Router>
</>
);
}
in this was way can edit the movie array in the APP Componenet
then your child component will be something like
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import './Topnav.css';
import { searchURL } from '../App';
function Topnav({onChange, query}) {
const [click, setClick] = useState(false)
const handleClick = () => setClick(!click)
const modeToggle = () => {
document.body.classList.toggle('dark')
}
return (
<>
<nav className="topnav">
<div className="topnav-container">
<Link to='/' className='topnav-logo'>
<img src={require('../img/logo.png').default} alt="logo" />
</Link>
<div className="wrapper">
<form id='search-bar'>
<input
type="text"
placeholder="Search"
className="search"
id="search"
value={query}
onChange={onChange}
/>
</form>
<label className="switch">
<input type="checkbox" id="mode-toggle" onChange={modeToggle}/>
</label>
</div>
...
So I want to toggle between different categories in my react movie-app such as Trending,Top Rated,Popular etc.I am use useState hook for this,by making the initial state as one category then changing the state through the onClick event on the buttons.But it doesn't seem to be working.What could be the problem?
Code:
App.js
import { useState } from "react";
import Movie from "./components/Movie";
import requests from "./components/ApiRequest";
import Navbar from "./components/Navbar";
function App() {
const [category, setCategory] = useState('top_rated')
return (
<div className="App">
<Navbar setCategory={setCategory} />
<div className="movie-container">
<Movie fetchUrl={"movie/" + category + "?api_key=" + API_KEY + "&language=en-US&page=1"} />
</div>
</div>
);
}
export default App;
Navbar.js
import React from 'react'
import SearchBar from './SearchBar'
import { FiFilter } from 'react-icons/fi'
const Navbar = ({ setCategory }) => {
return (
<div className="navbar-container">
<button className="navbar-btn"><FiFilter />Filter</button>
<div className="categories">
<button className="cat-btn" onClick={() => setCategory("popular")}>Popular</button>
<button className="cat-btn" onClick={() => setCategory("top_rated")}>Top Rated</button>
<button className="cat-btn" onClick={() => setCategory("upcoming")}>Upcoming</button>
</div>
<SearchBar />
</div>
)
}
export default Navbar
Movie.js
const Movie = ({ fetchUrl }) => {
const [movie, setMovie] = useState([]);
useEffect(() => {
async function getPost() {
const response = await client.get(fetchUrl);
console.log(response);
setMovie(response.data.results);
// return response;
}
getPost();
}, [])
return (
movie.map((m) => (
<div className="movie-component" key={m.id}>
<img src={`https://image.tmdb.org/t/p/w500${m.backdrop_path}`} alt="" />
<div className="metadata">
<h1>{m.title}</h1>
<a>⭐{m.vote_average}</a>
</div>
</div>
)
))
}
So I have initialized the useState hook in App.js and then using it in Navbar.js as the set the state of this hook on click event.
useEffect(() => {
async function getPost() {
const response = await client.get(fetchUrl);
console.log(response);
setMovie(response.data.results);
// return response;
}
getPost();
}, [fetchURL])
please update your dependency array as follows.
on changing the category, fetchURL value is being changed.
so it need to be included in dependency array of useEffect Hook.
I'm following a tutorial to make a React todo app.
I have components and contexts files.
I have addItem function but when I clicked 'Add todo' button,
the item and date is not rendering into todo list.
Also, it shows an error as Warning: Each child in a list should have a unique "key" prop. even though
I have given an id.
Since I am following the tutorial, I don't know where I did wrong.
Would be appreciated if anyone could tell what is wrong.
App.js
import React from 'react';
import Navbar from './components/Navbar';
import Form from './components/Form';
import TodoList from './components/TodoList';
import TodoContextProvider from './contexts/TodoContexts';
function App() {
return (
<div className="App">
<TodoContextProvider>
<Navbar />
<TodoList />
<Form />
</TodoContextProvider>
</div>
);
}
export default App;
TodoContexts.js
import React, { createContext, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
export const TodoContext = createContext();
const TodoContextProvider = (props) => {
const [items, setItems] = useState([
{items: 'laundry', date: '2020-11-18', id: 1},
{items: 'lunch', date: '2020-11-20', id: 2}
]);
const addItems = (items, date) => {
setItems([...items, {items, date, id: uuidv4()}]);
};
const removeItems = (id) => {
setItems(items.filter(item => item.id !== id));
};
return (
<TodoContext.Provider value={{ items, addItems, removeItems }}>
{props.children}
</TodoContext.Provider>
)
}
export default TodoContextProvider
TodoList.js
import React, { useContext } from 'react';
import TodoDetails from './TodoDetails';
import { TodoContext } from '../contexts/TodoContexts';
const TodoList = () => {
const { items } = useContext(TodoContext);
return items.length ? (
<div className="todo-list">
<ul>
{items.map(item => {
return ( <TodoDetails item={item} key={item.id} /> )
})}
</ul>
</div>
) : (
<div className="empty">You have no todos at the moment.</div>
)
}
export default TodoList
TodoDetails.js
import React, { useContext } from 'react';
import { TodoContext } from '../contexts/TodoContexts';
const TodoDetails = ({ item }) => { //TodoList item is props
const { removeItems } = useContext(TodoContext);
return (
<li onClick={() => removeItems(item.id)}>
<div className="items">{item.items}</div>
<div className="date">{item.date}</div>
</li>
)
}
export default TodoDetails
Form.js
import React, { useState, useContext } from 'react';
import './Form.css';
import { TodoContext } from '../contexts/TodoContexts';
const Form = () => {
const {addItems} = useContext(TodoContext);
const [items, setItems] = useState('');
const [date, setDate] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
console.log(items, date);
addItems(items, date);
setItems('');
setDate('');
}
return (
<form className="form" onSubmit={handleSubmit}>
<input
type="text"
value={items}
placeholder="Enter todo"
onChange={(e) => setItems(e.target.value)}
/>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<input type="submit" value="Add todo"/>
</form>
)
}
export default Form
Navbar.js
import React, { useContext } from 'react';
import { TodoContext } from '../contexts/TodoContexts';
const Navbar = () => {
const { items } = useContext(TodoContext);
return (
<div>
<h1>Todo List</h1>
<p>Currently you have {items.length} todos to get through...</p>
</div>
)
}
export default Navbar
Your error may be attributable to using same variable name of 'items' in addItems function:
Try changing the name of first argument to 'item' instead.
const addItems = (item, date) => {
setItems([...items, {item, date, id: uuidv4()}]);
};