I'm trying to implement some search features for an online book store with reactjs. I found a good repo that uses the google book api and decided to implement it on my project to see how it would work. But I'm getting the following error:
enter image description here
my github is: https://github.com/luismir15/CEN4010.git branch: luis
//code implemented by jason rivera
import React, { Component } from 'react';
import BookList from './BookList';
import SearchBox from './SearchBox';
import request from 'superagent';
class Books extends Component {
constructor(props){
super(props)
this.state = {
books: [],
searchField: '',
sort: ''
}
}
componentDidMount() {
request
.get("https://www.googleapis.com/books/v1/volumes")
.query({ q: this.state.searchField })
.then((data) => {
this.setState({ books: [...data.body.items] })
})
}
handleSubmit = (e) => {
e.preventDefault();
request
.get("https://www.googleapis.com/books/v1/volumes")
.query({ q: this.state.searchField })
.then((data) => {
console.log(data);
this.setState({ books: [...data.body.items] })
})
}
handleChange = (e) => {
this.setState({ searchField: e.target.value })
}
handleSort = (e) => {
this.setState({ sort: e.target.value});
}
render() {
const filteredBooks = this.state.books.sort((a, b) => {
const price1 = a.saleInfo.hasOwnProperty('listPrice') == false ? "$0.00" : a.saleInfo.listPrice.amount;
const price2 = b.saleInfo.hasOwnProperty('listPrice') == false ? "$0.00" : b.saleInfo.listPrice.amount;
if(this.state.sort == 'Newest'){
console.log("in newest")
return parseInt(b.volumeInfo.publishedDate.substring(0, 4)) - parseInt(a.volumeInfo.publishedDate.substring(0, 4));
}
else if(this.state.sort == 'Oldest'){
return parseInt(a.volumeInfo.publishedDate.substring(0, 4)) - parseInt(b.volumeInfo.publishedDate.substring(0, 4));
}
else if(this.state.sort == 'High'){
return parseInt(b.volumeInfo.averageRating) - parseInt(a.volumeInfo.averageRating);
}
else if(this.state.sort == 'Low'){
return parseInt(a.volumeInfo.averageRating) - parseInt(b.volumeInfo.averageRating);
}
else if(this.state.sort === 'Expensive'){
return parseInt(price2) - parseInt(price1);
}
else if(this.state.sort === 'Cheap'){
return parseInt(price1) - parseInt(price2);
}
return;
})
return (
<div className="wrapper">
<SearchBox
data={this.state}
handleSubmit={this.handleSubmit}
handleChange={this.handleChange}
handleSort={this.handleSort}
/>
<BookList books={filteredBooks}/>
</div>
);
}
}
export default Books;
import React, { Component } from 'react';
import BookCard from './BookCard';
const BookList = (props) => {
return (
<div className="list">
{
props.books.map((book) => {
return <BookCard key={book.id} info={book} />
})
}
</div>
);
}
export default BookList;
import React, { Component } from 'react';
const SearchBox = (props) => {
return (
<div className="search-area">
<form onSubmit={props.handleSubmit}>
<input onChange={props.handleChange} placeholder="Search books" type="text"/>
<button type="submit">Search</button>
<select value={props.sort} onChange={props.handleSort}>
<option value="" disabled selected>Sort</option>
<option value="Newest">Newest</option>
<option value="Oldest">Oldest</option>
<option value="High">High to Low</option>
<option value="Low">Low to High</option>
<option value="Expensive">$$$-$</option>
<option value="Cheap">$-$$$</option>
</select>
</form>
</div>
);
}
export default SearchBox;
import React, { Component } from 'react';
const BookCard = (props) => {
const { volumeInfo } = props.info;
const { saleInfo } = props.info;
const {title, authors, averageRating, subtitle, publishedDate} = props.info.volumeInfo;
const price = saleInfo.hasOwnProperty('listPrice') == false ? "$0.00" : saleInfo.listPrice.amount;
const thumbNail = volumeInfo.hasOwnProperty('imageLinks') == false ? "https://vignette.wikia.nocookie.net/pandorahearts/images/a/ad/Not_available.jpg/revision/latest?cb=20141028171337" : volumeInfo.imageLinks.thumbnail;
const publishYear = volumeInfo.hasOwnProperty('publishedDate') == false ? volumeInfo['publishedDate'] = "0000" : volumeInfo.publishedDate;
return (
<div className="card-container">
<img src={thumbNail} alt=""/>
<div className="desc">
<h2>{title}</h2>
<p>Author: {authors}</p>
<p>Price: {price}</p>
<p>Raiting: {averageRating == "0.00" ? "Not available" : averageRating}</p>
<p>Published: {publishYear == "0000" ? "Not available" : publishYear.substring(0,4)}</p>
</div>
</div>
);
}
export default BookCard;
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import '../style/browsing.css';
import Books from '../component/Books';
const Home = () => {
return (
<div>
<ul className="flexbox-container">
<div className="browsing">
<Books/>
</div>
</ul>
</div>
);
}
export default Home;
import React from 'react';
import './index.css';
import './style/browsing.css';
import Home from './pages/Home';
import Register from './pages/Register';
import Login from './pages/Login';
import Orders from './pages/Orders';
import BookDetails from './pages/BookDetails';
import ShopCart from './pages/ShopCart';
import Profile from './pages/Profile';
//import Books from './component/Books';
import { Route, HashRouter, NavLink } from 'react-router-dom';
// NPM RUN CLIENT is the updated src folder, (NPM RUN SERVER/NPM START) runs build in my case which is the old green template
//Use ctrL + C to stop the server
//Always run NPM INSTALL on a newly cloned file
//Do not push updates to master branch, push to your own branch PLZ
//updated file structure on my branch (miguel) 2/17/20
//npm install after downloading/ npm install --save react-bootstrap mighe be needed for BookDetails to work
//npm npm run client to run this package
const App = () => (
<div>
<HashRouter>
<div>
<NavLink to="/" style={{ textDecoration: 'none' }}>
<h1><i class="material-icons">menu_book</i> GeekText</h1>
</NavLink>
<ul className="header">
<li><NavLink exact to="/">Home</NavLink></li>
<li><NavLink to="/Login">Login</NavLink></li>
<li><NavLink to="/Orders">Orders</NavLink></li>
<li><NavLink to="/BookDetails">Book Details</NavLink></li>
<li><NavLink to="/Profile">Profile</NavLink></li>
<li>
<NavLink to="/ShopCart">
<i class="material-icons md-dark md-24">shopping_cart</i>
</NavLink>
</li>
</ul>
<div className="content">
<Route exact path="/" component={Home} />
<Route path="/Register" component={Register} />
<Route path="/Login" component={Login} />
<Route path="/Orders" component={Orders} />
<Route path="/BookDetails" component={BookDetails} />
<Route path="/ShopCart" component={ShopCart} />
<Route path="/Profile" component={Profile} />
</div>
</div>
</HashRouter>
</div>
);
export default App;
Related
Trying my hands at ReactJS fetch() examples. As mentioned this.props.history.push() not working, it is not giving an error, but it is simply not redirecting. Tried to read other answers to this question on StackOverflow, but many of them are either too complex(some ppl showing off) or some not very clear.
index.js:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { BrowserRouter as Router, NavLink, Switch, Route, useHistory } from "react-router-dom";
import reportWebVitals from "./reportWebVitals";
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById("root")
);
App.js :
import "./App.css";
import React from "react";
import { BrowserRouter as Router, NavLink, Switch, Route, useHistory } from "react-router-dom";
import RouterPage1 from "./RouterPage1";
import RouterPage2 from "./RouterPage2";
export default function App(props) {
let history = useHistory();
return (
<div>
<nav>
<ol>
<li>
<NavLink to="/RouterPage1">RouterPage1</NavLink>
</li>
<li>
<NavLink to="/RouterPage2">RouterPage2</NavLink>
</li>
<li>
<NavLink to="/RouterPageBoth">RouterPageBoth</NavLink>
</li>
</ol>
</nav>
<Switch>
<Route exact path="/RouterPage1">
<RouterPage1 history={history} />
</Route>
<Route exact path="/RouterPage2">
<RouterPage2 history={history} />
</Route>
</Switch>
</div>
);
}
RouterPage2.js(only the necessary code pasted, not the entire component for brevity):
addastakeholder = () => {
let newstakeholder = JSON.stringify(this.state.newstakeholder);
fetch("http://localhost:8080/OneToOneMappingPractice/add", {
method: "POST",
headers: { "Content-type": "application/json" },
body: newstakeholder,
}).then((r) => {
if (r.ok) {
//window.location.href = "/RouterPage2";
this.setState({ newstakeholder: { name: "", address: { house_number: "", streetName: "" } } });
this.props.history.push("/RouterPage2");
}
});
};
when I use the addastakeholder(), it is POSTing successfully, data is being entered in the DB, but it is not giving me an error and not redirecting. In the App.js, if I use the props and not useHistory(), it gives me "this.props.history not defined" error, even though I have enclosed App component in BrowserRouter component in the index.js.
Why is it so?
Secondly, if I use the commented out window.location.href = "/RouterPage2", it works(POSTing is successfull), but I am not able to see POST log in Development Tools:Network tab(Firefox). Why is this so?
Tried this.context.history.push("/RouterPage2"), does not work, same undefined error.
P.S.:edit 1:
the full RouterPage2.js(Kindly ignore result variable and the related code. Consider only result2.):
import React from "react";
export default class RouterPage2 extends React.Component {
constructor(props) {
super(props);
this.state = {
stakeholders: [],
errorString: "",
newstakeholder: { name: "", address: { house_number: "", streetName: "" } },
};
}
componentDidMount() {
fetch("http://localhost:8080/OneToOneMappingPractice/getAll")
.catch((error) => this.setState({ errorString: error }))
.then((result) => result.json())
.then((result) => this.setState({ stakeholders: result }));
}
addastakeholder = () => {
let newstakeholder = JSON.stringify(this.state.newstakeholder);
fetch("http://localhost:8080/OneToOneMappingPractice/add", {
method: "POST",
headers: { "Content-type": "application/json" },
body: newstakeholder,
}).then((r) => {
if (r.ok) {
//window.location.href = "/RouterPage2";
this.setState({ newstakeholder: { name: "", address: { house_number: "", streetName: "" } } });
this.props.history.push("/RouterPage2");
}
});
};
render() {
let result, result2;
let error = false;
if (this.state.stakeholders.length > 0)
result = (
<ol>
{this.state.stakeholders.map((stakeholder) => (
<li key={stakeholder.stakeholder_id}>
{stakeholder.stakeholder_name} | {stakeholder.stakeholder_type} |
{stakeholder.stakeholder_email_id} | {stakeholder.stakeholder_contactno} |
{stakeholder.stakeholder_bankname} | {stakeholder.stakeholder_bankBranch} |
{stakeholder.stakeholder_IFSC} | {stakeholder.stakeholder_AccNo} |
{stakeholder.stakeholder_AccType} | {stakeholder.stakeholder_PAN}
</li>
))}
</ol>
);
else result = false;
if (this.state.stakeholders.length > 0)
result2 = (
<ol>
{this.state.stakeholders.map((stakeholder) => (
<li key={stakeholder.id}>
{stakeholder.name}|{stakeholder.address.house_number}|{stakeholder.address.streetName}
</li>
))}
</ol>
);
else result2 = false;
if (this.state.errorString !== "undefined") error = this.state.errorString;
let blank = false;
if (result == false) blank = <h5>There are no records to display.</h5>;
return (
<div>
<h1>Stakeholder details :</h1>
{result2}
{error}
{blank}
<form>
Name :{" "}
<input
type="text"
placeholder="Name"
value={this.state.newstakeholder.name}
onChange={(e) => {
this.setState({ newstakeholder: { ...this.state.newstakeholder, name: e.target.value } });
}}
/>
<br></br>
StreetName :{" "}
<input
type="text"
placeholder="StreetName"
value={this.state.newstakeholder.address.streetName}
onChange={(e) => {
this.setState({
newstakeholder: {
...this.state.newstakeholder,
address: { ...this.state.newstakeholder.address, streetName: e.target.value },
},
});
}}
/>
<br></br>
HouseNumber :{" "}
<input
type="text"
placeholder="HouseNumber(Digits Only)"
value={this.state.newstakeholder.address.house_number}
onChange={(e) => {
this.setState({
newstakeholder: {
...this.state.newstakeholder,
address: { ...this.state.newstakeholder.address, house_number: e.target.value },
},
});
}}
/>
<br></br>
<button type="button" onClick={this.addastakeholder}>
Add Stakeholder
</button>
</form>
</div>
);
}
}
Tried everything. As suggested by Kurtis above in the comments, for troubleshooting purposes, did : console.log(this.props);
Got following response :
as you can see, there is the push function with different signature, hence tried : this.props.history.push("/RouterPage2", ""). Again did not work.
Hence, thought of trying the go(). And it worked.
this.props.history.go("/RouterPage2");
working now perfectly.
I try to keep in mind by refreshing the page the price of a product. The default value of this price comes from data retrieved from an API and I would like with an input text to change this price and keep it at the refresh of the page. I tried with localStorage so it retrieves the new price in the console application but when I refresh the page the price remains the default one and not the new price put in the input
I leave you my code below and I thank you in advance for your explanations
ProductsDetails :
import React, { Component } from 'react'
import '../css/ProductsDetails.css'
import {AiOutlineArrowLeft} from "react-icons/ai";
import {Link} from 'react-router-dom'
export default class ProductsDetails extends Component {
constructor(props) {
super(props);
this.state = {
id: this.props.match.params.id,
price: 0
};
}
updatePrice = (e) => {
localStorage.setItem('price', e.target.value)
this.setState({
price: e.target.value
});
};
submitHandler = (e) => {
e.preventDefault();
const {
match: {
params: { id }
}
} = this.props;
this.props.updatePrice(id, this.state.price);
};
componentDidMount() {
const price = localStorage.getItem('price')
this.setState({price});
}
render() {
const {
match: {
params: { id }
},
products
} = this.props;
const listProduct = products.find((product) => product.id === Number(id))
if (!listProduct) return null;
return (
<div className="products__details">
<Link to="/">
<AiOutlineArrowLeft className="nav__arrow" />
</Link>
<h1 className="details__title">{listProduct.title}</h1>
<div className="details__align--desk">
<div className="details__img">
<img
className="product__img"
src={listProduct.image}
alt="Affichage du produit"
/>
</div>
<div className="products__align--desk">
<h2 className="product__title">Description</h2>
<p className="product__description">{listProduct.description}</p>
<h2 className="product__title">Price</h2>
<form className="form__price" onSubmit={this.submitHandler}>
<input
name="price"
className="input__price"
type="text"
defaultValue={Number(listProduct.price).toFixed(2)}
onChange={this.updatePrice}
/>
<p>
Price (including VAT):{" "}
{Number(listProduct.price * 1.2).toFixed(2)} €
</p>
<br />
<input
className="btn__update"
type="submit"
value="Update product"
/>
</form>
</div>
<div className="category__align--desk">
<h2 className="product__title">Category</h2>
<p className={`${listProduct.category==="men's clothing" ? "category__orange" : "category__green"} product__category`}>{listProduct.category}</p>
</div>
</div>
</div>
);
}
}
Products :
import React, { Component } from 'react';
import '../css/Products.css';
import './ProductsDetails'
import {Link} from 'react-router-dom'
export default class Products extends Component {
render() {
const listsProducts = this.props.products.map((listProduct) => {
return (
<tbody className="products__body" key={listProduct.id}>
<tr>
<td><Link to={{pathname: "/products-details/" + listProduct.id}}>{listProduct.title}</Link></td>
<td><p className={`${listProduct.category==="men's clothing" ? "category__orange" : "category__green"}`}>{listProduct.category}</p></td>
<td>{Number(listProduct.price).toFixed(2)}</td>
<td>
{Number(listProduct.price * 1.2).toFixed(2)} €
</td>
</tr>
</tbody>
);
});
return (
<main className="products">
<h1 className="products__title">Products management</h1>
<table cellSpacing="0">
<thead className="products__head">
<tr>
<th className="table--title">Product name</th>
<th className="table--title">Category</th>
<th className="table--title">Price</th>
<th className="table--title">Price (including VAT)</th>
</tr>
</thead>
{listsProducts}
</table>
</main>
);
}
}
App :
import React, { useState, useEffect } from 'react';
import './css/App.css';
import './css/Products.css';
import axios from 'axios';
import {Oval} from 'react-loading-icons'
import Navigation from './Components/Navigation';
import Products from './Components/Products';
import {BrowserRouter as Router, Route, Switch,} from 'react-router-dom';
import ProductsDetails from './Components/ProductsDetails';
export default function App() {
const [error, setError] = useState(null);
const [productsData, setProductsData] = useState([]);
const [isLoaded, setIsLoaded] = useState(false);
const updatePrice = (id, price) => {
setProductsData((productsData) =>
productsData.map((product) =>
product.id === Number(id)
? {
...product,
price: Number(price)
}
: product
)
);
};
useEffect(() => {
axios.get("https://fakestoreapi.com/products?limit=7").then((res) => {
setIsLoaded(true);
setProductsData(res.data);
},
(error) => {
setIsLoaded(true);
setError(error);
}
);
}, []);
if (error) {
return <div>Erreur : {error.message}</div>
} else if (!isLoaded) {
return <div>
<Oval stroke="#564AFF"/>
<p>Loading...</p>
</div>
} else {
return (
<div className="App">
<Router>
<Navigation/>
<Switch>
<Route
path="/products-details/:id"
render={(props) => (
<ProductsDetails
products={productsData}
updatePrice={updatePrice}
{...props}
/>
)}
/>
<Route path="/">
<Products products={productsData}
/>
</Route>
</Switch>
</Router>
</div>
)
}
}
I am trying to make my App.js route to my People.jsx etc.. but it is not working correctly. I hope I could fix the issue from there if I could make this work. I have been trying to do this for about 2 hours with the 20 min rule but this one I need help with. I have tried other variations but my goal is to get the,theID over to Person as well. I am thinking about using {useContext } to do that but I can't even get it to route. I wish I knew what I was doing wrong so I could correct it but other people are using different types of routers and I was confused with them even more.
I updated it with links still a no go for me any other suggestions?
App.js
import './App.css';
import { useState } from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import People from './components/People'
import Planet from './components/Planets'
import Starship from './components/Starships'
import { Router, Link } from '#reach/router';
function App() {
const [starwarsState, setStarwarsState] = useState('')
const [theID, setTheID] = useState('')
const selectedState = (e) => {
setStarwarsState(e.target.value)
}
const switchItem = () => {
switch (starwarsState) {
case 'people':
<Link path='/people/' />;
break;
case 'planets':
<Link path="/planets/" />;
break;
case 'starships':
<Link path='/starships/' />;
break;
default:
return null;
}
}
const addId = e => {
setTheID(e.target.value)
console.log(theID)
}
return (
<div className='App'>
<header className='App-header' >
Search For:
<select onChange={selectedState} className='form-control-lg bg-dark text-white'>
<option value='people' active >People</option>
<option value='planets' >Planets</option>
<option value='starships' >Starships</option>
</select>
ID:
<input type='text' onChange={addId} className='form-control-lg col-sm-1 bg-dark text-white' />
<button className='btn-lg btn-warning' onClick={switchItem} >Search Item</button>
<Router>
<People path='/people/' />
<Planet path="/planets/" />
<Starship path='/starships/' />
</Router>
</header>
{starwarsState}
</div>
)
}
export default App;
People.jsx
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import { Link } from '#reach/router';
const People = props => {
const [peopleData, setpeopleData] = useState([]);
useEffect(() => {
axios.get(`https://swapi.dev/api/people/${props.theID}`)
.then(response => { setpeopleData(response.data) })
console.log(peopleData)
}, []);
return (
<div>
<span> the People have spoken</span>
<Link to='/people' />
</div>
)
}
export default People;
Issues
You aren't actually rendering the routes/links from switchItem since onClick callbacks can't return renderable UI directly to the render method.
Solution
Unconditionally render your routes all at the same time within a single Router and imperatively navigate to them in the switchItem handler.
App
...
import { Router, navigate } from "#reach/router";
...
function App() {
const [starwarsState, setStarwarsState] = useState("");
const [theID, setTheID] = useState("");
...
const switchItem = () => {
switch (starwarsState) {
case "people":
navigate("/people"); // <-- imperative navigation
break;
case "planets":
navigate("/planets");
break;
case "starships":
navigate("/starships");
break;
default:
return null;
}
};
return (
<div className="App">
<header className="App-header">
Search For:
<select
onChange={selectedState}
value={starwarsState}
className="form-control-lg bg-dark text-white"
>
<option disabled value="">
Choose Path
</option>
<option value="people">
People
</option>
<option value="planets">Planets</option>
<option value="starships">Starships</option>
</select>
ID:
<input
type="text"
onChange={addId}
className="form-control-lg col-sm-1 bg-dark text-white"
/>
<button className="btn-lg btn-warning" onClick={switchItem}>
Search Item
</button>
</header>
<Router>
<People path="/people" theID={theID} /> // <-- pass `theID` state as prop
<Planet path="/planets" />
<Starship path='/starships' />
</Router>
</div>
);
}
People
const People = ({ theID }) => {
const [peopleData, setpeopleData] = useState([]);
useEffect(() => {
axios.get(`https://swapi.dev/api/people/${theID}`)
.then(response => { setpeopleData(response.data) });
}, [theID]);
return (
<div>
<div>The ID: {theID}</div>
<span>the People have spoken</span>
</div>
);
};
Use Imperative Routing (not switch statement) with Event Handlers
Your code is using a switch statement in combination with the switchItem() function. This is not how to redirect the user imperatively (meaning, through something other than a link clicked directly by the user).
To imperatively route your users, use the navigate method.
Via Reach Router docs (link):
Sometimes you need to navigate in response to something other than the user clicking on a link. For this we have navigate. Let’s import navigate.
import {
Router,
Link,
navigate
} from "#reach/router"
In your case, the entire switch statement can be rewritten as follows:
useEffect(() => navigate(`/${starwarsState}`), [starwarsState])
useEffect will watch for changes to the starwarsState, which is either going to be 'people', 'planets', or 'starships'. Once a change occurs, it will navigate the user to the corresponding path.
Solution: Routing Only
The following solution doesn't implement axios, it focuses solely on the client-side routing logic.
I found some other issues with your code when I was working through a solution. Here is a version that I wrote that implements parameter-level routing while also making some other cleanup (refactoring the swapi categories into a config object, etc).
App.js
import React, { useState, useEffect } from 'react'
import { useWhatChanged } from "#simbathesailor/use-what-changed";
import { Router, Link, navigate } from "#reach/router";
import 'bootstrap/dist/css/bootstrap.min.css';
import { People, Person } from './components/People'
import { Planets, Planet } from './components/Planets'
import { Starships, Starship } from './components/Starships'
import './App.css';
function App() {
// destructure categories from config
const { people, planets, starships } = config.categories
// initialize state
const [starwarsState, setStarwarsState] = useState(people);
const [theID, setTheID] = useState('');
// log updates to ID and starwarsState
useWhatChanged([starwarsState, theID], 'starwarsState, theID')
// change state on input from user
const addId = e => setTheID(e.target.value)
const selectCategory = (e) => setStarwarsState(e.target.value)
// route the user based on starwarsState
useEffect(() => navigate(`/${starwarsState}`), [starwarsState])
// search swapi based on category and id
const searchSwapi = e => {
e.preventDefault()
navigate(`/${starwarsState}/${theID}`)
}
return (
<div className="App">
<header className='App-header' >
Search For:
<form onSubmit={searchSwapi}>
<select onChange={selectCategory} className='form-control-lg bg-dark text-white'>
<option value={people} >People</option>
<option value={planets} >Planets</option>
<option value={starships} >Starships</option>
</select>
ID:
<input type='text' onChange={addId} className='form-control-lg col-sm-1 bg-dark text-white' />
<button className='btn-lg btn-warning' >Search Item</button>
</form>
</header>
<Router>
<People path='/people/'>
<Person path=':personId' />
</People>
<Planets path="/planets/">
<Planet path=':planetId' />
</Planets>
<Starships path='/starships/'>
<Starship path=':starshipId' />
</Starships>
</Router>
</div>
)
}
const config = {
categories: {
people: 'people',
planets: 'planets',
starships: 'starships'
}
}
export default App;
Planets.js
import React from 'react'
export const Planets = props => {
return (
<div>
<span> the Planets have spoken</span>
{props.children}
</div>
)
}
export const Planet = props => {
return (
<div>
Planet Data
</div>
)
}
People.js
import React, { useState, useEffect } from 'react'
export const People = props => {
return (
<div>
<span> the People have spoken</span>
{props.children}
</div>
)
}
export const Person = props => {
return (
<div>
Person Data
</div>
)
}
Starships.js
import React from 'react'
export const Starships = props => {
return (
<div>
<span> the Starships have spoken</span>
{props.children}
</div>
)
}
export const Starship = props => {
return (
<div>
Starship Data
</div>
)
}
[UPDATE] Solution: Routing with API Calls
The following solution takes the code from above and refactors it using the state management pattern proposed by Leigh Halliday. The solution adds three things:
useContext for managing global state
React.memo() for memoizing AppContent component
react-query for managing remote API calls to SWAPI.
View code on GitHub
App.js
// App.js
import React, {
useState,
useEffect,
createContext,
useContext,
memo
} from 'react'
import { ReactQueryDevtools } from "react-query-devtools";
import { useWhatChanged } from "#simbathesailor/use-what-changed";
import { Router, navigate } from "#reach/router";
import 'bootstrap/dist/css/bootstrap.min.css';
import { People, Person } from './components/People'
import { Planets, Planet } from './components/Planets'
import { Starships, Starship } from './components/Starships'
import './App.css';
import Axios from 'axios';
// APP w/ CONTEXT PROVIDER
export default function App() {
return (
<>
<StarwarsProvider>
<AppContent />
</StarwarsProvider>
<ReactQueryDevtools initialIsOpen={false} />
</>
)
}
// CREATE CONTEXT
export const StarwarsContext = createContext()
// CONTEXT PROVIDER
function StarwarsProvider({ children }) {
// import categories
const categories = config.categories
// destructure default category of search selection
const { people } = categories
// initialize state
const [category, setCategory] = useState(people);
const [theID, setTheID] = useState('');
return (
<StarwarsContext.Provider value={{
category,
setCategory,
theID,
setTheID,
categories,
fetchStarwarsData
}}>
<AppContent />
</StarwarsContext.Provider>
)
}
async function fetchStarwarsData(category, id) {
if (!id) {
return
}
const response = await Axios.get(
`https://swapi.dev/api/${category}/${id}`
).then(res => res.data)
// const data = await response.json()
const data = response
// console.log(data)
return data
}
// APP CONTENT
const AppContent = memo(() => {
// import global state into component
const { category, setCategory } = useContext(StarwarsContext)
const { theID, setTheID } = useContext(StarwarsContext)
// destructure categories
const { categories: { people, planets, starships } } = useContext(StarwarsContext)
// log updates to ID and category
useWhatChanged([category, theID], 'category, theID')
// change state on input from user
const addId = e => setTheID(e.target.value)
const selectCategory = (e) => setCategory(e.target.value)
// route the user based on category
useEffect(() => navigate(`/${category}`), [category])
// search swapi based on category and id
const searchSwapi = e => {
e.preventDefault()
navigate(`/${category}/${theID}`)
}
return (
<div className="App">
<header className='App-header' >
Search For:
<form onSubmit={searchSwapi}>
<select onChange={selectCategory} className='form-control-lg bg-dark text-white'>
<option value={people} >People</option>
<option value={planets} >Planets</option>
<option value={starships} >Starships</option>
</select>
ID:
<input type='text' onChange={addId} className='form-control-lg col-sm-1 bg-dark text-white' />
<button className='btn-lg btn-warning' >Search Item</button>
</form>
</header>
<Router>
<People path='/people/'>
<Person path=':personId' fetchStarwarsData />
</People>
<Planets path="/planets/">
<Planet path=':planetId' fetchStarwarsData />
</Planets>
<Starships path='/starships/'>
<Starship path=':starshipId' fetchStarwarsData />
</Starships>
</Router>
</div>
)
})
const config = {
categories: {
people: 'people',
planets: 'planets',
starships: 'starships'
}
}
People.js
// People.js
import React from 'react'
import { useQuery } from "react-query";
import { StarwarsContext, StarwarsProvider } from "../App"
export const People = props => {
return (
<div>
<span> the People have spoken</span>
{props.children}
</div>
)
}
export const Person = () => {
const { category, theID, fetchStarwarsData } = React.useContext(StarwarsContext)
const { data, isLoading, error } = useQuery([category, theID], fetchStarwarsData)
if (isLoading) return <div>loading...</div>
if (error) return <div>oop!! error ocurred</div>
return (
<div>
<h1>/{category}/{theID}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
Planets.js
// Planets.js
import React from 'react'
import { useQuery } from "react-query";
import { StarwarsContext, StarwarsProvider } from "../App"
export const Planets = props => {
return (
<div>
<span> the Planets have spoken</span>
{props.children}
</div>
)
}
export const Planet = props => {
const { category, theID, fetchStarwarsData } = React.useContext(StarwarsContext)
const { data, isLoading, error } = useQuery([category, theID], fetchStarwarsData)
if (isLoading) return <div>loading...</div>
if (error) return <div>oop!! error ocurred</div>
return (
<div>
<h1>/{category}/{theID}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
Starships.js
// Starships.js
import React from 'react'
import { useQuery } from "react-query";
import { StarwarsContext, StarwarsProvider } from "../App"
export const Starships = props => {
return (
<div>
<span> the Starships have spoken</span>
{props.children}
</div>
)
}
export const Starship = () => {
const { category, theID, fetchStarwarsData } = React.useContext(StarwarsContext)
const { data, isLoading, error } = useQuery([category, theID], fetchStarwarsData)
if (isLoading) return <div>loading...</div>
if (error) return <div>oop!! error ocurred</div>
return (
<div>
<h1>/{category}/{theID}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
I have been trying to learn React over the past couple of weeks and started working on a site which displays art works.
I would like for the user to be able to click on one of the images displayed and for a new component to be loaded with information about the work.
I have the implementation below of the gallery view, but when I click on an image the URL changes, but the WorkPage component never loads.
Would anyone be able to spot what I am doing wrong? The links and images are generated in the renderItems() function.
import React, { Component } from "react";
import Masonry from 'react-masonry-css';
import WorkPage from "./WorkPage"
import axios from "axios";
import { Link, Route, Switch, useRouteMatch, useParams } from "react-router-dom";
import { BrowserRouter as Router } from "react-router-dom";
class Works extends Component {
constructor(props) {
super(props);
this.state = {
viewPaintings: true,
workList: []
};
axios
.get("http://localhost:8000/api/works/")
.then(res => this.setState({ workList: res.data }))
.catch(err => console.log(err))
};
displayPaintings = status => {
if (status) {
return this.setState({ viewPaintings: true })
}
return this.setState({ viewPaintings: false })
};
renderTabList = () => {
return (
<div>
<ul className="tab-list-buttons">
<li onClick={() => this.displayPaintings(true)}
className={this.state.viewPaintings ? "active" : "disabled"}
key="button1"
>
Paintings
</li>
<li onClick={() => this.displayPaintings(false)}
className={this.state.viewPaintings ? "disabled" : "active"}
key="button2"
>
Works on Paper
</li>
</ul>
</div>
);
};
renderItems = () => {
const { viewPaintings } = this.state;
const newItems = viewPaintings
? this.state.workList.filter(item => item.type === 1)
: this.state.workList.filter(item => item.type === 0);
const breakpointColumnsObj = {
default: 4,
1100: 3,
700: 2,
500: 1
};
const items = newItems.map(item => (
<div key = {item.slug}>
<Link to={`${item.slug}`}>
<img src={item.image} alt={item.name} width="300"/>
</Link>
<Switch>
<Route path=":item.slug" component={WorkPage} />
</Switch>
</div>
));
return (
<Masonry
breakpointCols={breakpointColumnsObj}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{items}
</Masonry>
);
}
render() {
return (
<Router>
<div>
{ this.renderTabList() }
{ this.renderItems() }
</div>
</Router>
)
};
}
export default Works;
I’m new to react and I’m currently facing an issue.
I want to make a conditional rendering such that if the URL contains a particular id I render something else but my components seems to be doing basically everything multiple times if this condition is met. Below is my code
import React, { useState, useEffect } from 'react';
import DriverList from './DriverList';
import './Drivers.css';
import getDrivers from '../../Helpers/fetchAny';
function Drivers(props) {
const url = window.location.href.split('/');
const last = url.length - 1;
const urlPattern = /([\d\w]*[-]).*/g;
const id = url[last];
const [driverState, setDriverState] = useState({ name: 'Hello'
});
const [toDisplayState, setToDisplayState] = useState([]);
useEffect(() => {
getDrivers('/api/drivers').then(data => {
setDriverState(data);
});
}, []);
if (!urlPattern.test(url)) {
return (
<div className="mainBody drivers">
<div className="driver-display" />
<DriverList />
</div>
);
} else {
return (
<div>
<p>Hello</p>
</div>
);
}
}
export default Drivers;
The question is if this condition is not met, it renders what it’s supposed to normally not repeating anything but once (normal) when the condition is met.
The component
import React, { useState, useEffect } from 'react';
import getDrivers from '../../../Helpers/getDrivers';
import { Link } from 'react-router-dom';
function DriverList() {
const [driverState, setDriverState] = useState([]);
useEffect(() => {
getDrivers().then(data => {
let driverDetails = [];
for (const driver of data) {
driverDetails.push({
driverId: driver.driverID,
driverName: driver.name,
driverPhone: driver.phone,
});
}
setDriverState(driverDetails);
});
}, []);
return (
<div className="driver-list">
<p className="list-head">Drivers</p>
{driverState.map((driver, index) => {
return (
<Link
to={`/drivers/${driver.driverId}`}
className="single-list"
key={index}
>
<span className="list-image" />
<div>
<span>
<i className="mdi mdi-account" /> {driver.driverName}
</span>
<span>
<i className="mdi mdi-phone" /> {driver.driverPhone}
</span>
</div>
</Link>
);
})}
</div>
);
}
export default DriverList;
This shows the list of drivers
So apparently, I had repeated the route to that page in my App.js
<Route exact path="/" component={Home} />
<Route path="/dashboard" component={Home} />
<Route path="/trips" component={Trips} />
<Route path="/drivers" component={Drivers} />
<Route path="/drivers:driverId” component={Drivers} />
All I had to do was remove onE of the routes.
My understanding from this is that react renderswhen you visit aroute` in a SPA (single page application)