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} />
Related
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);
The code below is a react component called Start. Its function is to take in a players name via a form with onSubmit. It stores the players name in a hook called "player". Then it passes the player name prop to another component called GameBoard. Once the submit button is pressed the browser navigates to the GameBoard component via react-router-dom. The GameBoard Component is then supposed to display the players name that passed to it in the Start component.
The issue I'm having is that the player name state is not being passed into the GameBoard component. When onSubmit is initiated the page changes to the GameBoard but the player name doesn't get passed. Any ideas?
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
//styles
import { Wrapper, Content } from "../styles/start.styles";
//component
import GameBoard from "../components/gameBoard.component.jsx";
const Start = (props) => {
const [player, setPlayer] = useState("");
let handleChange = (event) => {
event.preventDefault();
setPlayer(event.target.value);
};
let handleSubmit = () => {
if (player === "") {
alert("Please enter a players name");
} else {
window.history.replaceState(null, "GameBoard", "/gameboard");
}
};
useEffect(() => {
console.log(player);
});
return (
<Router>
<Switch>
<Route exact path="/">
<Wrapper>
<Content>
<h1>Trivia Words</h1>
<h2>Start Menu</h2>
<form onSubmit={handleSubmit}>
<label>Enter Player Name:</label>
<input type="text" onChange={handleChange}></input>
<input type="submit" value="Start"></input>
</form>
</Content>
</Wrapper>
</Route>
<Route exact path="/gameboard">
<GameBoard playerName={player} />
</Route>
</Switch>
</Router>
);
};
export default Start;
GameBoard Component below
import React, { useState } from "react";
//styles
import { Wrapper, Content } from "../styles/gameBoard.styles";
const GameBoard = (props) => {
const [playerName, setPlayerName] = useState(props.playerName);
const [letters, setLetters] = useState([]);
const [triviaQA, setTriviaQA] = useState([]);
const [gameOver, setGameOver] = useState(false);
return (
<Wrapper>
<Content>Player Name: {playerName}</Content>
</Wrapper>
);
};
export default GameBoard;
creating a derived state from props is in general bad practice. you should consume your props directly, and if you need to update its state at Child Component you should pass a setState prop as well:
import React, { useState } from "react";
//styles
import { Wrapper, Content } from "../styles/gameBoard.styles";
const GameBoard = (props) => {
const [letters, setLetters] = useState([]);
const [triviaQA, setTriviaQA] = useState([]);
const [gameOver, setGameOver] = useState(false);
return (
<Wrapper>
<Content>Player Name: {props.playerName}</Content>
</Wrapper>
);
};
export default GameBoard;
also, you seem not using the proper navigation from react-router-dom at your handleSubmit.
you could create a Form Player component and import useHistory and push to the desired route to be able to use useHistory or wrap your component with BrowserRouter:
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Switch, Route, useHistory } from "react-router-dom";
//styles
import { Wrapper, Content } from "../styles/start.styles";
//component
import GameBoard from "../components/gameBoard.component.jsx";
const Start = (props) => {
const [player, setPlayer] = useState("");
let handleChange = (event) => {
event.preventDefault();
setPlayer(event.target.value);
};
useEffect(() => {
console.log(player);
});
return (
<Router>
<Switch>
<Route exact path="/">
<Wrapper>
<Content>
<h1>Trivia Words</h1>
<h2>Start Menu</h2>
<PlayerNameForm player={player} handleChange={handleChange} />
</Content>
</Wrapper>
</Route>
<Route exact path="/gameboard">
<GameBoard playerName={player} />
</Route>
</Switch>
</Router>
);
};
export default Start;
const PlayerNameForm = ({player, handleChange}) => {
const history = useHistory();
let handleSubmit = () => {
if (player === "") {
alert("Please enter a players name");
} else {
history("/gameboard");
}
};
return (
<form onSubmit={handleSubmit}>
<label>Enter Player Name:</label>
<input type="text" onChange={handleChange}></input>
<input type="submit" value="Start"></input>
</form>
)
}
Main.js
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 [selectedBook, setSelectedBook] = useState() // Storing the data from Books.js
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={selectedBook}/> // is defined here <----
<div className='container'>
<Switch>
<Route exact path='/' >
<Books books={books} onBookSelect={(book) => {setSelectedBook(book)} }/> // Receiving the data from here
</Route>
<Route path='/add-book' >
<AddBook />
</Route>
<Route path={`/details`} >
<BookDetail bookId={selectedBook} /> // is undefined when passed here <----
</Route>
</Switch>
</div>
</div>
</Router>
)
}
export default Main
Books.js
import React, { Component, useState } from 'react'
import BookDetail from './BookDetail'
const Books = ({books, onBookSelect}) => {
const sendBookId = e => {
let bookId = e.target.value
onBookSelect('i am not undefined') // sending the data to Main.js
setTimeout(function(){ window.location.href='http://localhost:3000/details' }, 10)
}
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.id} onClick={sendBookId}>View Details</button> // Getting the id from here
</div>
))}
</div>
</div>
)
}
export default Books
BookDetail.js
import React, { useEffect } from 'react'
import axios from 'axios'
import { useState } from 'react'
const BookDetail = ({bookId}) => {
return (
<div>
<p>book: {bookId}</p> // bookId is undefined
</div>
)
}
export default BookDetail
Sample from localhost:8000/api/get-books
{
"id": 5,
"name": "Book1",
"author": "Author1",
"content": "a normal book",
"isbn": "235456",
"passcode": "123",
"created_at": "2021-07-12T16:29:47.114356Z",
"pages": "3"
}
Basically, the data is sent from Book.js to the parent component which is Main.js and which is stored in selectedBook, and the data is defined and displays in the title, but when I add it as a prop in and try to access it from there it becomes undefined, what am I doing wrong?
Issue
Credit to Yoshi for calling it out first, but there is an issue with the way you navigate from your home page to the book details page. Mutating the window.location.href object will actually reload the page, and your app. Since React state lives in memory it is lost upon page reloading.
Solution
Use the history object provided by the Router context to PUSH to the new route. Since you are already using a function component you can import the useHistory React hook from react-router-dom and issue the imperative navigation.
import { useHistory } from 'react-router-dom';
...
const Books = ({ books, onBookSelect }) => {
const history = useHistory(); // <-- access the history object from hook
const sendBookId = (e) => {
const bookId = e.target.value;
onBookSelect(bookId);
history.push("/details"); // <-- issue PUSH to route
};
return (
<div>
<div className="books">
Books
{books.map((book) => (
<div className="book">
<h2>{book.name}</h2>
<p>
Author: {book.author}
</p>
<button className="btn" value={book.id} onClick={sendBookId}>
View Details
</button>
</div>
))}
</div>
</div>
);
};
I'm attempting to link to somewhere within my application using react-router-dom within an appBar/header that is persistent throughout the app. I keep getting "TypeError: history is undefined" when I attempt to use RRD within the header component.
I've been playing around with this for a good few hours now and I'm not getting any where with it. I can't think straight thanks to the heat, and I'm clearly searching for the wrong answers in my searches. The best solution I have come-up with thus-far is having each component contain the header component at the top but this is obv not ideal. I know I must be missing something simple as this can't be an uncommon pattern.
Demo Code
Node Stuff
npx create-react-app rdr-header --template typescript
npm install react-router-dom
App.tsx
import React from "react";
import "./App.css";
import {
BrowserRouter as Router,
Switch,
Route,
useHistory,
} from "react-router-dom";
function App() {
let history = useHistory();
const handleClick = (to: string) => {
history.push(to);
};
return (
<div className='App'>
<header className='App-header'>
<button onClick={() => handleClick("/ger")}>German</button>
<button onClick={() => handleClick("/")}>English</button>
</header>
<Router>
<Switch>
<Route exact path='/' component={English} />
<Route path='/ger' component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push("/ger");
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push("/");
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
You should create separate component for header
header.js
import React from 'react';
import './style.css';
import { useHistory } from 'react-router-dom';
function Header() {
let history = useHistory();
const handleClick = to => {
history.push(to);
};
return (
<header className="App-header">
<button onClick={() => handleClick('/ger')}>German</button>
<button onClick={() => handleClick('/')}>English</button>
</header>
);
}
export default Header;
Use Header component inside Router like below:-
import React from 'react';
import './style.css';
import {
BrowserRouter as Router,
Switch,
Route,
useHistory
} from 'react-router-dom';
import Header from './header.js'; // import header component
function App() {
return (
<div className="App">
<Router>
<Header /> // use Header component inside Router
<Switch>
<Route exact path="/" component={English} />
<Route path="/ger" component={German} />
</Switch>
</Router>
</div>
);
}
const English = () => {
let history = useHistory();
const handleClick = () => {
history.push('/ger');
};
return (
<>
<h1>English</h1>
<button onClick={handleClick}>Go to German</button>
</>
);
};
const German = () => {
let history = useHistory();
const handleClick = () => {
history.push('/');
};
return (
<>
<h1>German</h1>
<button onClick={handleClick}>Go to English</button>
</>
);
};
export default App;
Instead of changing the history object using history.push(), you can use the <Link> or <NavLink> components from react-router.
React Router - Link component
Make sure to place the header component inside the Router component.
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]);