Fairly new to react here. I'm making a small recipe finder app with an api. After getting the data, I'm mapping through the results and displaying them in a component. What I want to do is display the details of each recipe through another component in another route. I'm not sure how to do this. I thought I could pass the mapped recipe through Link, but it's not working. Here is what I have so far.
Index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById('root')
);
App.js
import React, { useState, useEffect} from "react";
import axios from "axios";
import { BrowserRouter as Router, Routes, Route, useNavigate} from "react-router-dom";
import "./App.css";
import RecipeList from "./RecipeList";
import Recipe from "./Recipe";
import Header from "./Header";
function App() {
const navigate = useNavigate();
const [recipes, setRecipes] = useState([]);
const [query, setQuery] = useState("");
const [search, setSearch] = useState("");
const APP_ID = "XXXXXXXXX";
const APP_KEY = "XXXXXXXXXXXXXXXXXXXXXX";
const url = `https://api.edamam.com/api/recipes/v2?type=public&q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`;
const getRecipes = async () => {
const res = await axios(url);
const data = await res.data.hits;
console.log(data);
setRecipes(data);
};
useEffect(() => {
getRecipes();
}, [query]);
const updateSearch = (e) => {
setSearch(e.target.value);
console.log(search);
};
const getSearchQuery = (e) => {
e.preventDefault();
setQuery(search);
setSearch("");
navigate("/recipes");
};
return (
<div className="App">
<Header />
<div>
<div className="container">
<form className="search-form" onSubmit={getSearchQuery}>
<input
className="search-input"
type="text"
value={search}
onChange={updateSearch}
placeholder="search by food name"
/>
</form>
</div>
</div>
<Routes>
<Route path="/recipes" element={<RecipeList recipes={recipes} />} />
<Route path="/recipes/:id" element={<Recipe recipes={recipes} />}/>
</Routes>
</div>
);
}
export default App;
RecipeList.jsx
import React from "react";
import { Link } from "react-router-dom";
const RecipeList = ({ recipes }) => {
return (
<div className="container">
<div className="grid-container">
{recipes.map(({ recipe }) => (
<Link to={`/recipes/${recipe.label}`}>
<img key={recipe.image} src={recipe.image} alt="" />
<p key={recipe.label}>{recipe.label}</p>
<p>{recipe.id}</p>
</Link>
))}
</div>
</div>
);
};
export default RecipeList;
Recipe.jsx
const Recipe = ({recipe}) => {
return (
<div>
<h1>{recipe.label}</h1>
</div>
)
}
export default Recipe
Am I even close???
You are passing the entire recipes array to both routed components.
<Routes>
<Route path="/recipes" element={<RecipeList recipes={recipes} />} />
<Route path="/recipes/:id" element={<Recipe recipes={recipes} />}/>
</Routes>
So Recipe can use the entire array and the id route match param to search the passed array and render the exact recipe by matching label.
import { useParams } from 'react-router-dom';
const Recipe = ({ recipes }) => {
const { id } = useParams();
const recipe = recipes.find(recipe => recipe.label === id); // *
return recipe ? (
<div>
<h1>{recipe.label}</h1>
</div>
) : null;
};
* Note: Since you call the route param id it may make more sense to us the recipe.id for the link.
{recipes.map(({ recipe }) => (
<Link to={`/recipes/${recipe.id}`}>
<img key={recipe.image} src={recipe.image} alt="" />
<p key={recipe.label}>{recipe.label}</p>
<p>{recipe.id}</p>
</Link>
))}
...
const recipe = recipes.find(recipe => recipe.id === id);
Related
Fairly new to react and trying to get the router to work for me. I'm pulling in data from a third party api using a simple form. When the data is retrieved I'd like to display it in a new route, and then ultimately have another route for each item retrieved. When I submit the form I just get a '?' in the route params. If I enter the route manually then submit the form the data displays. How can I get the data to display on form submit?
import axios from "axios";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import "./App.css";
import RecipeList from "./RecipeList";
import Header from "./Header";
function App() {
const [recipes, setRecipes] = useState([]);
const [query, setQuery] = useState("");
const [search, setSearch] = useState("");
const APP_ID = "XXXXXXXX";
const APP_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const url = `https://api.edamam.com/api/recipes/v2?type=public&q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`;
const getRecipes = async () => {
const res = await axios(url);
const data = await res.data.hits;
console.log(data);
setRecipes(data);
};
useEffect(() => {
getRecipes();
}, [query]);
const updateSearch = (e) => {
setSearch(e.target.value);
console.log(search);
};
const getSearchQuery = () => {
e.preventDefault();
setQuery(search);
setSearch("");
};
return (
<div className="App">
<Header />
<div>
<div className="container">
<form className="search-form" onSubmit={getSearchQuery}>
<input
className="search-input"
type="text"
value={search}
onChange={updateSearch}
placeholder="search by food name"
/>
<button className="search-button" type="submit">
Search Recipes
</button>
</form>
</div>
</div>
{/* <RecipeList recipes={recipes}/> */}
<Router>
<Routes>
<Route path="/recipes" element={<RecipeList recipes={recipes}/>} />
<Route path="/recipes/:id" />
</Routes>
</Router>
</div>
);
}
export default App;
import React from "react";
import { Link } from "react-router-dom";
const RecipeList = ({ recipes }) => {
console.log(recipes);
return (
<div>
{recipes.map(({ recipe }, id) => (
<Link to={`recipes/${recipe.label}`}>
<p key={id}>{recipe.label}</p>
</Link>
))}
</div>
);
};
export default RecipeList;
If I understand your question/issue you are having issue linking to a specific recipe. I suspect it is because you are using a relative link, so you are linking to a "/recipes/recipes/<label".
Either use absolute link paths, i.e. using a leading "/":
<Link to={`/recipes/${recipe.label}`}>
<p key={id}>{recipe.label}</p>
</Link>
Or use a correct relative path, i.e. only append the next level path segment, in other words, appending recipe.label to "/recipes":
<Link to={`${recipe.label}`}>
<p key={id}>{recipe.label}</p>
</Link>
If you wanting to start on "/" and submit the form and navigate to "/recipes" then issue an imperative navigation after submitting the form. Import the useNavigate hook to issue the imperative navigation and move the Router to wrap the App component so the routing context is provided to it and the useNavigate hook can work properly.
import axios from "axios";
import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom";
import "./App.css";
import RecipeList from "./RecipeList";
import Header from "./Header";
function App() {
const navigate = useNavigate(); // <-- use navigate hook
const [recipes, setRecipes] = useState([]);
const [query, setQuery] = useState("");
const [search, setSearch] = useState("");
const APP_ID = "XXXXXXXX";
const APP_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
const url = `https://api.edamam.com/api/recipes/v2?type=public&q=${query}&app_id=${APP_ID}&app_key=${APP_KEY}`;
const getRecipes = async () => {
const res = await axios(url);
const data = await res.data.hits;
console.log(data);
setRecipes(data);
};
useEffect(() => {
getRecipes();
}, [query]);
const updateSearch = (e) => {
setSearch(e.target.value);
console.log(search);
};
const getSearchQuery = () => {
e.preventDefault();
setQuery(search);
setSearch("");
navigate("/recipes"); // <-- imperative navigation
};
return (
<div className="App">
<Header />
<div>
<div className="container">
<form className="search-form" onSubmit={getSearchQuery}>
<input
className="search-input"
type="text"
value={search}
onChange={updateSearch}
placeholder="search by food name"
/>
<button className="search-button" type="submit">
Search Recipes
</button>
</form>
</div>
</div>
<Routes>
<Route path="/recipes" element={<RecipeList recipes={recipes}/>} />
<Route path="/recipes/:id" />
</Router>
</div>
);
}
index.js
<Router>
<App />
</Router>
Main.js (Parent)
import axios from 'axios'
import React, { Component, useEffect, useState } from 'react'
import { BrowserRouter as Router, Switch, Route} from "react-router-dom";
import Books from './Books'
import Navbar from './Navbar'
import AddBook from './AddBook';
import BookDetail from './BookDetail';
const Main = () => {
let [books, setBooks] = useState([])
useEffect(() => {
axios.get('http://127.0.0.1:8000/api/get-books')
.then(res => {
setBooks(books = res.data)
})
})
return (
<Router>
<div>
<Navbar title='LunaBooks'/>
<div className='container'>
<Switch>
<Route exact path='/' >
<Books books={books} />
</Route>
<Route path='/add-book' >
<AddBook />
</Route>
<Route path='/details/' >
<BookDetail bookName={} /> // put the bookName from Books.js here <----
</Route>
</Switch>
</div>
</div>
</Router>
)
}
export default Main
Books.js (Child)
import React, { Component, useState } from 'react'
import BookDetail from './BookDetail'
const Books = ({books}) => {
let [bookName, setBookName] = useState('') // send the bookname to Main.js
const SendBookDetails = e => {
let bookName = e.target.value
setBookName(bookName = bookName)
}
return (
<div>
<div className='books'>
{books.map(book => (
<div className='book'>
<h2>{book.name}</h2>
<p>Author: <a href='#'>{book.author}</a></p>
<button className="btn" value={book.name} onClick={SendBookDetails}>View Details</button>
</div>
))}
</div>
</div>
)
}
export default Books
To put it simply, I want to send the bookName that is located in Books.js and put in the Main.js so that I can pass it in BookDetail.js, I know how to do in the reverse way but this is just confusing me for some reason... I'm pretty new to react so please bear with me!
Make a selectedBook state in the parent.
const [selectedBook, setSelectedBook] = useState()
Pass a callback function as a prop "onBookSelect" to the child.
<Books books={books} onBookSelect={(book) => { setSelectedBook(book) } />
Then in the child call the callback function with new value. in your SendBookDetails function.
const Books = ({books, onBookSelect}) => {
//...
const SendBookDetails = e => {
let bookName = e.target.value
setBookName(bookName)
onBookSelect(bookName)
}
Then pass that state to the props of your book details component.
<BookDetail bookName={selectedBook} />
I have created a blog post. Posts are in the card[Event.js] and on click of the button. It should go to the new page and render its card details there. How can I do it using the react hooks and useParams.?
EventList.js ---> is where I'm fetching the data from api
Event.js ---> I'm rendering the fetched data in cards
EventDetails.js ---> It's the card details that should render on the screen when clicked on the post. Right now I have hard coded. the details.
Could someone please help me with how to do this?
//EventList.js
import React, { useState, useEffect } from "react";
import Event from "../Event/Event";
import axios from "axios";
import styles from "./EventList.module.css";
const EventList = () => {
const [posts, setPosts] = useState("");
let config = { Authorization: "..........................." };
const url = "............................................";
useEffect(() => {
AllPosts();
}, []);
const AllPosts = () => {
axios
.get(`${url}`, { headers: config })
.then((response) => {
const allPosts = response.data.articles;
console.log(response);
setPosts(allPosts);
})
.catch((error) => console.error(`Error: ${error}`));
};
return (
<div>
<Event className={styles.Posts} posts={posts} />
</div>
);
};
export default EventList;
//Event.js
import React from "react";
import styles from "./Event.module.css";
import { Link } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
const Event = (props) => {
const displayPosts = (props) => {
const { posts } = props;
if (posts.length > 0) {
return posts.map((post) => {
return (
<div>
<div>
<div className={styles.post}>
<img
src={post.urlToImage}
alt="covid"
width="100%"
className={styles.img}
/>
<div>
<h3 className={styles.title}>{post.title}</h3>
<div className={styles.price}> {post.author} </div>
<Link to={`/${post.title}`}>
<button className={styles.btns}> {post.author} </button>
</Link>
</div>
</div>
</div>
</div>
);
});
}
};
return <div className="Posts">{displayPosts(props)}</div>;
};
export default Event;
//EventDetails.js
import React, { useState, useEffect } from "react";
import Navbar from "../Navbar/Navbar";
import DetailsImage from "../../assets/Event-Ticketing.png";
import styles from "./EventDetails.module.css";
import "bootstrap/dist/css/bootstrap.min.css";
import { Link, useParams, useLocation } from "react-router-dom";
import axios from "axios";
// let config = { Authorization: "3055f8f90fa44bbe8bda05385a20690a" };
// const baseurl = "https://newsapi.org/v2/top-headlines?sources=bbc-news";
const EventDetails = (props) => {
const { state } = useLocation();
if (!state) return null;
// const [title, setTitle] = useState("");
// const { eventName } = useParams();
// useEffect(() => {
// axios
// .get(`${baseurl}`, { headers: config })
// .then((response) => setTitle(response.data));
// }, []);
// useEffect(() => {
// const neweventName = baseurl.find(
// (eventNames) => eventNames.eventName === parseInt(eventName)
// );
// setTitle(neweventName.title);
// }, []);
return (
<div>
<Navbar />
<div className={styles.eventBg}>
<div className="container">
<div>
<img
src={DetailsImage}
alt="ticket"
width="100%"
className={styles.heroEventImage}
/>
</div>
<div className={styles.bookingDetails}>
<div className={styles.nameBook}>
<div>
<div className={styles.eventNameHeader}>
<h1 className={styles.eventName}> {props.title}</h1>
</div>
<div className={styles.genre}>
<div className={styles.genreText}>{props.author}</div>
</div>
</div>
<div className={styles.bookingBtn}>
<div className={styles.booking}>
<Link to="/GeneralBooking">
<button
className={styles.bookBtn}
style={{ height: "60px", fontSize: "18px" }}
>
Book
</button>
</Link>
</div>
</div>
</div>
<div className={styles.venueTime}>
<div className={styles.dateTime}>
<div className={styles.dateTimeText}>{props.author}</div>
<div className={styles.price}>{props.author}</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default EventDetails;
//App.js
import "./App.css";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Home from "./components/Home/Home";
import EventDetails from "./components/EventDetails/EventDetails";
import GeneralBooking from "./components/GeneralBooking/GeneralBooking";
import AllotedSeated from "./components/AllotedSeated/AllotedSeated";
import Checkout from "./components/Checkout/Checkout";
function App() {
return (
<BrowserRouter>
<div className="App">
<Switch>
<Route path="/" exact>
<Home />
</Route>
<Route path="/:title" exact children={<EventDetails />}></Route>
<Route path="/GeneralBooking" exact>
<GeneralBooking />
</Route>
</Switch>
{/* <EventDetails /> */}
{/* <GeneralBooking /> */}
{/* <AllotedSeated /> */}
{/* <Checkout /> */}
</div>
</BrowserRouter>
);
}
export default App;
Since it doesn't appear as though you've stored the posts state sufficiently high enough in the ReactTree to be accessible by component on other routes I suggest using route state to send a specific post object to a receiving route.
Event - Update the Link to send also the post object.
<Link
to={{
pathname: `/${post.title}`,
state: { post },
}}
>
<button type="button" className={styles.btns}>{post.author}</button>
</Link>
EventDetails - Use the useLocation hook to access the route state.
import { useLocation } from "react-router-dom";
const EventDetails = (props) => {
const { state } = useLocation();
if (!state.post) return null;
return (
// ...render all the post fields available from state.post
// i.e. state.post.title
);
};
I am building a small React Routing application to get a better idea as to how it work. My App.js looks like this with the basic routing:
import React from 'react';
import './App.css';
import Nav from './Nav';
import About from './About';
import Shop from './Shop';
import CountryDetail from './CountryDetail'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
function App() {
return (
<Router>
<div className="App">
<Nav />
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={About} />
<Route path="/shop" exact component={Shop} />
<Route path="/shop/:name" component={CountryDetail} />
</Switch>
</div>
</Router>
);
}
const Home = () => (
<div>
<h1>Home Page</h1>
</div>
);
Now the Shop component a list of countries from the api which is in the code below:
import React from 'react';
import './App.css';
import { useEffect } from 'react';
import { useState } from 'react';
import {Link} from 'react-router-dom';
function Shop() {
useEffect(() => {
fetchItems();
},[])
const [countries, setCountries] = useState([])
const fetchItems = async () => {
const data = await fetch('https://restcountries.eu/rest/v2/all');
const countries = await data.json();
console.log(countries);
setCountries(countries);
}
return (
<div>
{countries.map(country => (
<div>
<Link to={`shop/${country.name}`}>
<h1 key={country.alpha2Code}>
{country.name}
</h1>
</Link>
<p>Popluation {country.population}</p>
<p> Region {country.region}</p>
<p>Capital {country.capital}</p>
</div>
)
)}
</div>
);
}
export default Shop;
Now what I want to do is render more information about the country when I click on it. So I have created another component called CountryDetail:
import React from 'react';
import './App.css';
import { useEffect } from 'react';
import { useState } from 'react';
function CountryDetail( { 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 country = await fetchCountry.json();
setCountry(country);
console.log(country);
}
return (
<div>
<h1>Name {country.name}</h1>
<p>Native Name{country.nativeName}</p>
<p>Region {country.region}</p>
<p>Languages {country.languages}</p>
<h1>This Country</h1>
</div>
);
}
export default CountryDetail;
The problem I am having is that it is not rendering anything on the CountryDetail page. I am sure I have hit the api correctly but not sure if I am getting the data correctly. Any help would be appreciated.
Issue: The returned JSON is an array but your JSX assumes it is an object.
Solution: You should extract the 0th element from the JSON array. Surround in a try/catch in case of error, and correctly render the response.
Note: the languages is also an array, so that needs to be mapped
function CountryDetail({ match }) {
useEffect(() => {
fetchItem();
console.log(match);
}, []);
const [country, setCountry] = useState(null);
const fetchItem = async () => {
try {
const fetchCountry = await fetch(
`https://restcountries.eu/rest/v2/name/${match.params.name}`
);
const country = await fetchCountry.json();
setCountry(country[0]);
console.log(country[0]);
} catch {
// leave state alone or set some error state, etc...
}
};
return (
country && (
<div>
<h1>Name {country.name}</h1>
<p>Native Name{country.nativeName}</p>
<p>Region {country.region}</p>
<p>Languages {country.languages.map(({ name }) => name).join(", ")}</p>
<h1>This Country</h1>
</div>
)
);
}
As you said it yourself, the response is an array (with a single country object in it), but you are using it as if it would be an object.
So, instead of:
const country = await fetchCountry.json();
setCountry(country);
It should be:
const countries = await fetchCountry.json();
setCountry(countries[0]);
I am trying to create a shared global state for all components that an app needs, and instead of relying on props drilling or redux, I am trying to achieve that with the React Context.
Why does my user context not survive when I switch between routes? The application bellow illustrates the issue.
Do I need to use any other hook in conjunction with useContext?
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { AuthenticationProvider } from "./AuthenticationProvider";
const Index = () => {
return (
<AuthenticationProvider>
<App />
</AuthenticationProvider>
);
}
ReactDOM.render(<Index />, document.getElementById('root'));
//App.js
import React, { useState, useContext } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import './App.css';
import { AuthenticationContext } from './AuthenticationProvider';
function AddUser() {
const [formUser, setFormUser] = useState("");
const [user, setUser] = useContext(AuthenticationContext);
const handleSubmit = async (event) => {
event.preventDefault();
setUser(formUser);
}
return (
<React.Fragment>
Form user: {formUser}.
<form id="form1" onSubmit={handleSubmit}>
<input type="text" id="user" onChange={event => setFormUser(event.target.value)} />
<input type="submit" value="Save" />
</form>
<br/>
Current user: {user}
<br/>
Back to home
</React.Fragment>
);
}
function Home() {
const [user, setUser] = useContext(AuthenticationContext);
return (
<React.Fragment>
<div className="App">
Hello {user}.
<br/>
Add user
</div>
</React.Fragment>
);
}
function App() {
return (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/add" component={AddUser} />
</Switch>
</Router>
);
}
export default App;
//AuthenticationProvider.js
import React, { useState, createContext } from "react";
const DEFAULT_STATE = "";
export const AuthenticationContext = createContext(DEFAULT_STATE);
export const AuthenticationProvider = ({ children }) => {
const [user, setUser] = useState(DEFAULT_STATE);
return (
<AuthenticationContext.Provider value={[user, setUser]} >
{children}
</AuthenticationContext.Provider>
);
}
The problem is that you used a regular <a> link to navigate through the app and every time you go from Home to addUser the app refreshes. To navigate through the app without refreshing the page use the Link component from react-router-dom
in Home and AddUser change the a links to the Link component
import { Link } from "react-router-dom";
function Home() {
const { user, setUser } = useContext(AuthenticationContext);
return (
<React.Fragment>
<div className="App">
Hello {user}.
<br />
<Link to="/add">Add user</Link> <-- Changed a to Link
</div>
</React.Fragment>
);
}
function AddUser() {
const [formUser, setFormUser] = useState("");
const [user, setUser] = useContext(AuthenticationContext);
const handleSubmit = async (event) => {
event.preventDefault();
setUser(formUser);
}
return (
<React.Fragment>
Form user: {formUser}.
<form id="form1" onSubmit={handleSubmit}>
<input type="text" id="user" onChange={event => setFormUser(event.target.value)} />
<input type="submit" value="Save" />
</form>
<br />
Current user: {user}
<br />
<Link to="/">Back to home</Link> <-- Changed a to Link
</React.Fragment>
);
}