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>
)
}
Related
My context is as follows:
import React, {createContext, useEffect, useState} from "react";
export const CartContext = createContext();
const CartContextProvider = (props) => {
const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart')) || []);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const updateCart = (productId, op) => {
let updatedCart = [...cart];
if (updatedCart.find(item => item.id === productId)) {
let objIndex = updatedCart.findIndex((item => item.id === productId));
if (op === '-' && updatedCart[objIndex].qty > 1) {
updatedCart[objIndex].qty -= 1;
} else if (op === '+') {
updatedCart[objIndex].qty += 1;
}
} else {
updatedCart.push({id: productId, qty: 1})
}
setCart(updatedCart);
}
const removeItem = (id) => {
setCart(cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{cart, updateCart, removeItem}}>
{props.children}
</CartContext.Provider>
)
};
export default CartContextProvider;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NavigationBar from "./components/layout/navigationBar/NavigationBar";
import Homepage from "./pages/homepage/Homepage";
import AboutUsPage from "./pages/aboutUs/AboutUsPage";
import ContactPage from "./pages/contact/ContactPage";
import SearchPage from "./pages/search/SearchPage";
import ShoppingCart from "./components/layout/shoppingCart/ShoppingCart";
import CartContextProvider from "./context/CartContext";
function App() {
return (
<div>
<CartContextProvider>
<Router>
<NavigationBar/>
<ShoppingCart/>
<Routes>
<Route exact path="/" element={<Homepage/>}/>
<Route path="/a-propos" element={<AboutUsPage/>} />
<Route path="/contact" element={<ContactPage/>}/>
<Route path="/recherche" element={<SearchPage/>}/>
</Routes>
</Router>
</CartContextProvider>
</div>
);
}
export default App;
In the component ShoppingCart I am using another component ShoppingCartQuantity which in turn makes use of the context. It works as it should.
Here's the ShoppingCartQuantity component:
import React, {useContext} from "react";
import {CartContext} from "../../../context/CartContext";
import styles from './ShoppingCartQuantity.module.css'
const ShoppingCartQuantity = ({productId}) => {
const {cart, updateCart} = useContext(CartContext);
let qty = 0;
if (cart.find((item => item.id === productId))) {
let objIndex = cart.findIndex((item => item.id === productId));
qty = cart[objIndex].qty;
}
return (
<div>
<span>
<span className={`${styles.op} ${styles.decrementBtn}`} onClick={() => updateCart(productId, '-')}>-</span>
<span className={styles.qty}>{qty}</span>
<span className={`${styles.op} ${styles.incrementBtn}`} onClick={() => updateCart(productId, '+')}>+</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
Now I am trying to use the ShoppingCartQuantity component in the Homepage component which is a route element (refer to App.js) but getting the error Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.
So the context is working for components outside the router but not for those inside it. If I have wrapped the router within the provider, shouldn't all the route elements get access to the context or am I missing something?
UPDATE
As user Build Though suggested in the comments, I tried using the ShoppingCartQuantity component in another route element and it works fine; so the problem is not with the router!
Below is the code of how I am using the ShoppingCartQuantity component in the Homepage component:
import React, { useState, useEffect, useRef } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import Subcat from "../../components/subcat/Subcat";
import CategoryService from "../../services/api/Category";
import SubCategoryService from "../../services/api/SubCategory";
import CategoriesLayout from "../../utils/CategoriesLayout";
import CategoryCard from "../../components/category/CategoryCard";
import { Triangle } from 'react-loader-spinner'
import ScrollIntoView from 'react-scroll-into-view'
import ProductService from "../../services/api/Product";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content';
import YouTube from 'react-youtube';
import FavoriteBtn from "../../components/favorite/FavoriteBtn";
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
import "./Homepage.css";
import "../../components/product/ProductModal.css"
import "react-loader-spinner";
import modalStyles from "../../components/product/ProductModal.module.css"
function Homepage() {
const [categories, setCategories] = useState([]);
const [subCats, setSubCats] = useState([]);
const [loader, setLoader] = useState(false);
const ResponsiveGridLayout = WidthProvider(Responsive);
const scrollRef = useRef();
const productModal = withReactContent(Swal);
const opts = {
// height: '390',
// width: '640',
playerVars: {
autoplay: 1,
}
};
useEffect(() => {
CategoryService.get().then((response) => {
setCategories(response);
});
}, []);
function showSubCatsHandler(catId) {
setLoader(true);
setSubCats([]);
SubCategoryService.get(catId).then((response) => {
setSubCats(response.data);
setLoader(false);
scrollRef.current.scrollIntoView({ behavior: "smooth" });
});
}
function showProductPopupHandler(productId) {
ProductService.get(productId).then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
<h3 className={modalStyles.header}>{product.AMP_Title}</h3>
<h4 className={`${modalStyles.price} ${modalStyles.header}`}>{"CHf " + product.AMP_Price}</h4>
<img className={modalStyles.image} src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/products/' + product.AMP_Image} />
{
product.descriptions.map((desc, _) => (
<div key={desc.AMPD_GUID}>
{
desc.AMPD_Title === '1' && <h4 className={modalStyles.header}>{product.AMP_Title}</h4>
}
{
desc.AMPD_Image !== '' && <img src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/descriptions/' + desc.AMPD_Image} className={desc.AMPD_Alignment === 'left' ? modalStyles.descImageLeft : modalStyles.descImageRight} />
}
<p className={modalStyles.description}>{desc.AMPD_Description}</p>
</div>
))
}
<br/>
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity productId={product.AMP_GUID} />
</div>
<br/>
{
product.AMP_VideoId !== '' &&
<YouTube
videoId={product.AMP_VideoId}
opts={opts}
/>
}
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (
<div>
<div className="categories-container">
<ResponsiveGridLayout
className="layout"
layouts={ CategoriesLayout }
breakpoints={ { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } }
cols={ { lg: 8, md: 8, sm: 6, xs: 4, xxs: 2 } }
isDraggable={ false }
>
{
categories.map((cat, index) => (
<div key={index}>
<CategoryCard
category_id = {cat.AMC_GUID}
image = {cat.AMC_Image}
showSubCatsHandler = {showSubCatsHandler}
/>
</div>
))
}
</ResponsiveGridLayout>
{
loader &&
<Triangle
height="100"
width="100"
color='#bcad70'
ariaLabel='loading'
wrapperClass="loader"
/>
}
<div ref={scrollRef}>
{
Object.keys(subCats).map((keyName, _) => (
<Subcat
key={subCats[keyName].AMSC_GUID}
title={ subCats[keyName].AMSC_Title }
products={ subCats[keyName].products }
showProductPopupHandler = {showProductPopupHandler}
/>
))
}
</div>
</div>
</div>
);
}
export default Homepage;
I am using the component in a SweetAlert popup. I guess it's the SweetAlert component that is not getting access to the context. Does anyone have an idea how to pass the context to the SweetAlert component?
UPDATE 2
The accepted solution works great except for 1 small issue: the ShoppingCartQuantity component was not re-rendering inside the SweetAlert popup and the qty would not change visually.
I updated the component by using the qty as a state.
const ShoppingCartQuantity = ({ qty, productId, updateCart }) => {
const [quantity, setQuantity] = useState(qty);
const updateCartHandler = (productId, amount) => {
updateCart(productId, amount);
setQuantity(Math.max(quantity + amount, 1));
}
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCartHandler(productId, -1)}
>
-
</span>
<span className={styles.qty}>{quantity}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCartHandler(productId, 1)}
>
+
</span>
</span>
</div>
)
}
Issue
It's very likely that the sweet alert component is rendered outside your app, and thus, outside the CartContextProvider provider. I just searched the repo docs if there is a way to specify a root element, but this doesn't seem possible since this sweet alert code isn't React specific.
See this other similar issue regarding accessing a Redux context in the alert.
Solution
It doesn't seem possible ATM to access the context value from within the modal, so IMHO a workaround could be to refactor your ShoppingCartQuantity component into a wrapper container component to access the context and a presentation component to receive the context values and any callbacks.
I suggest also just passing the amount you want to increment/decrement the quantity by to updateCart instead of passing a "+"/"-" string and operator comparison.
Example:
export const withShoppingCartContext = Component => props => {
const { cart, removeItem, updateCart } = useContext(CartContext);
return <Component {...props} {...{ cart, removeItem, updateCart }} />;
}
const ShoppingCartQuantity = ({ cart, productId, updateCart }) => {
const qty = cart.find(item => item.id === productId)?.qty ?? 0;
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCart(productId, -1)}
>
-
</span>
<span className={styles.qty}>{qty}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCart(productId, 1)}
>
+
</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
In places in your app where ShoppingCartQuantity component is used within the CartContextProvider decorate it with the withShoppingCartContext HOC and use normally.
ShoppingCart
import ShoppingCartQuantityBase, {
withShoppingCartContext
} from "../../components/layout/shoppingCart/ShoppingCartQuantity";
const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);
const ShoppingCart = (props) => {
...
return (
...
<ShoppingCartQuantity productId={....} />
...
);
};
In places where ShoppingCartQuantity component is used outside the context, like in the sweet modal, access the context within the React code and pass in the context values and callbacks.
...
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
...
function Homepage() {
...
const { cart, updateCart } = useContext(CartContext);
const productModal = withReactContent(Swal);
...
function showProductPopupHandler(productId) {
ProductService.get(productId)
.then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
...
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity
productId={product.AMP_GUID}
{...{ cart, updateCart }}
/>
</div>
...
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (...);
}
export default Homepage;
Additional Issues
Your context provider is mutating state when updating quantities. When updating nested state you should still create a shallow copy of the array elements that are being updated.
Example:
const CartContextProvider = (props) => {
...
const updateCart = (productId, amount) => {
// only update if item in cart
if (cart.some(item => item.id === productId)) {
// use functional state update to update from previous state
// cart.map creates shallow copy of previous state
setCart(cart => cart.map(item => item.id === productId
? {
...item, // copy item being updated into new object reference
qty: Math.max(item.qty + amount, 1), // minimum quantity is 1
}
: item
));
}
}
const removeItem = (id) => {
setCart(cart => cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{ cart, updateCart, removeItem }}>
{props.children}
</CartContext.Provider>
);
};
You did't show where you are using the ShoppingCart component or the ShoppingCartQuantity component.
Anyway, when you declare a route, you must pass the component, not the root element. So, this line:
<Route exact path="/" element={<Homepage/>}/>
must be
<Route exact path="/" component={Homepage}/>
I have React Components:
My account.jsx
import "./myAccount.sass";
import { auth } from "../../firebaseConfig";
import {
signOut,
onAuthStateChanged,
} from "https://www.gstatic.com/firebasejs/9.6.0/firebase-auth.js";
import { useEffect, useState } from "react";
import { Hotel } from "../Hotel/Hotel";
function MyAccount(props) {
const { isauth } = props;
const { hotels = [] } = props;
const [userEmail, setUserEmail] = useState([]);
function logout() {
signOut(auth);
isauth(false);
console.log("Successful logout");
}
function userInfo() {
onAuthStateChanged(auth, (user) => {
console.log(`user info: ${user.email}"`);
setUserEmail(user.email);
});
}
useEffect(() => {
userInfo();
}, []);
return (
<div>
<h3>Hi, {userEmail}</h3>
<button onClick={logout}>Logout...</button>
<h4>Your reviews:</h4>
<hr />
<div className="Hotels">
{hotels.map((hotel, id) => (
<Hotel key={id} {...hotel} filter={true} user={userEmail} />
))}
</div>
</div>
);
}
export default MyAccount;
Hotels.jsx
import starsMap from "../../starsMap";
import { useEffect, useState } from "react";
import "./hotel.sass";
function Hotel(props, filter = false, user = null) {
const { name, img, localization, stars, review, author } = props;
const [userEmail, setUserEmail] = useState(user);
if (filter) {
console.log(author, userEmail);
if (author === user) {
return (
<div className="Hotel_card">
<h2 className="Hotel_card-name">{name}</h2>
<div className="Hotel_card_wrapper">
<img className="Hotel_card-img" src={img} alt="hotel_img" />
<h3 className="Hotel_card-localization">
{/* Lat:{localization._lat}
Long:{localization._lat} */}
<button>Show on map</button>
</h3>
{starsMap.get(stars)}
</div>
<p className="Hotel_card-review">{review}</p>
<h5 className="Hotel_card-author">Wroten by {author}</h5>
</div>
);
}
} else {
return (
<div className="Hotel_card">
<h2 className="Hotel_card-name">{name}</h2>
<div className="Hotel_card_wrapper">
<img className="Hotel_card-img" src={img} alt="hotel_img" />
<h3 className="Hotel_card-localization">
{/* Lat:{localization._lat}
Long:{localization._lat} */}
<button>Show on map</button>
</h3>
{starsMap.get(stars)}
</div>
<p className="Hotel_card-review">{review}</p>
<h5 className="Hotel_card-author">Wroten by {author}</h5>
</div>
);
}
}
export { Hotel };
The MyAccount.jsx component passes two attributes to the Hotels.jsx component - filter={true} user={userEmail}.
Hotels.jsx only sees filter={true}, user remains null for it.
Question - why doesn't Hotels.jsx see the user={userEmail} passed to it?
If you look at the state userEmail in MyAccount.jsx, then the desired value is found this way, but it is lost at the moment it is transferred to Hotels.jsx
It's because you are using an intermediate useState in the Hotel.jsx file and you are not receiving correctly the props
const [userEmail, setUserEmail] = useState(user);
remove that line (it seems that you are not using it anyway) and change in the Hotels.jsx component the prop user to userEmail:
function Hotel({filter = false, userEmail = null, ...props})
A React Functional Component, is a function that returns some JSX code. When you write it like:
<Hotel key={id} {...hotel} filter={true} user={userEmail} />
This is JSX, and this code gets converted to React first class API:
React.createElement(
Hotel,
[props],
[...children]
)
props are filter, user and whatever comes out of {...hotel}.
The React.createElement just takes props and children ( if at all ), and passes an object to your Hotel function with them all inside, remember that props is an object:
Hotel(props)
Being an object, when you want to recall your props from within your Component, you have two possible ways:
function Hotel (props) {
console.log(props.filter, props.user)
}
or through destructuring them directly when declaring them:
function Hotel ({filter, user}) {
console.log(filter, user)
}
If you are passing other props that you don't want to name directly:
function Hotel ({filter, user, ...props}) {
console.log(filter, user, props)
}
I've got some dynamic buttons that are a child component, and get assigned a value="URL" based off of my MongoDB. How do I go about passing that generated value to my parent/web component src={currentSrc}? When I assign it, it says that currentSrc is not defined?
Here's the SizeOptions:
import { useState } from "react";
export const SizeOptions = ({ size }) => {
const sizeName = Object.keys(size);
// Update Model Viewers Src
function changeSize (){
setSrc(currentSrc)
console.log(currentSrc)
}
if (!sizeName) return <></>;
return (
<div>
{size[sizeName].map((item) => (
<button key={item} className='size' value={item} onClick={changeSize} currentSrc={item}>
{sizeName}
</button>
))}
</div>
);
};
And here is the ProductScreen:
import './ProductScreen.css';
import { useEffect, useState } from "react";
//Components
import { SizeOptions } from '../components/SizeOptions';
const [currentSrc, setSrc] = useState(size[sizeName][0])
const ProductScreen = ({match}) => {
return(
<div className='sizebuttons'>
{product && (product.size || []).map((size, index) => (<SizeOptions key={index} size={size} setSrc={currentSrc} changeSize={changeSize}/>))}
</div>
<div className="productscreen__right">
<model-viewer
id="model-viewer"
src={currentSrc}
alt={product.productName}
ar
ar-modes="scene-viewer quick-look"
ar-placement="floor"
shadow-intensity="1"
camera-controls
min-camera-orbit={product.mincameraorbit}
max-camera-orbit={product.maxcameraorbit}
interaction-prompt="none">
<button slot="ar-button" className="ar-button">
View in your space
</button>
</model-viewer>
</div>
)}
The rendered button:
I have more code inside my ProductScreen, I just tried to keep it as minimized as possible to try and make it easier to help me figure it out! Any help would be greatly appreciated!
Good news! I finally figured it out after many, many attempts.
So in order to pass from child to parent this is what I came up with:
SizeOptions.js
export const SizeOptions = ({ size, changeSrc }) => {
const sizeName = Object.keys(size);
if (!sizeName) return <></>;
return (
<div>
{size[sizeName].map((url) => (
<button key={url} className='size' value={url} onClick={() => changeSrc(url)}>
{sizeName}
</button>
))}
</div>
);
};
export default SizeOptions;
ProductScreen.js
import './ProductScreen.css';
import { useEffect, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
// Actions
import { getProductDetails } from "../redux/actions/productActions";
//Components
import { SizeOptions } from '../components/SizeOptions';
const ProductScreen = ({match}) => {
const dispatch = useDispatch();
const productDetails = useSelector(state => state.getProductDetails);
const { loading, error, product } = productDetails;
const [ src, setSrc ] = useState("Default URL")
return(
<div className='sizebuttons'>
{product && (product.size || []).map((size, index) => (<SizeOptions key={index} size= {size} changeSrc={src => setSrc(src)}/>))}
</div>
<model-viewer
id="model-viewer"
src={src}
alt={product.productName}
ar
ar-modes="scene-viewer quick-look"
ar-placement="floor"
shadow-intensity="1"
camera-controls
min-camera-orbit={product.mincameraorbit}
max-camera-orbit={product.maxcameraorbit}
interaction-prompt="none">
<button slot="ar-button" className="ar-button">
View in your space
</button>
</model-viewer>
)};
The only issue now is how to make the "Default URL" actually be a URL as right now if I put {src} there, it says it cannot do that before the component is rendered.
I use a color theme in my project for this, I used a React context to pass the value in several components, everything worked fine for me until I decided to add another property inside the object in order to apply different colors to different components, for example a ThemeBackground property that takes green as a value it will be applied to the RoutesPage component and a side property that takes an orange color it will be applied to the SideBar component. The problem is that I cannot apply the side property for the SideBar component, I tried several options, but I did not succeed right now, I will show you everything in more detail in the pictures so that you clearly understand the problem and then I will provide you with the code
Notice the ThemesBackground property is successfully applied to the content but the problem is that I want to apply the side property to my sidebar at the moment I imported the ThemeBackground property for my sidebar so my sidebar applies red color but I think you already understood the problem in short property ThemeBackground should be applied to content and side property to sidebar
LessonThemes.jsx
import React, { useState, useEffect, createContext } from "react";
import SideBar from "./SideBar";
import RoutesPage from "../pages/Routes";
export const CounterContext = createContext(["color"]);
export default function LessonThemes(props) {
const [BackgroundTheme, SetBackgroundTheme] = useState(localStorage.getItem("color"));
const [themes, setThemes] = useState([
{ name: "G", ThemeBackground: "maroon", side: "orange" },
{ name: "R", ThemeBackground: "red", side: "aqua" },
{ name: "B", ThemeBackground: "blue", side: "pink" },
])
useEffect(() => {
localStorage.setItem("color", BackgroundTheme);
})
const SideBarPageContent = (SideBarPageContentBackground) => {
localStorage.setItem('color', SideBarPageContentBackground);
SetBackgroundTheme(SideBarPageContentBackground);
}
const list = themes.map((theme, index) => {
return (
<label key={index}>
<input
onChange={() => SideBarPageContent(theme.ThemeBackground)}
type="radio"
name="background"
/>{theme.name}</label>
);
})
return (
<CounterContext.Provider value={[BackgroundTheme, SetBackgroundTheme]}>
<SideBar list={list} {...props} />
<RoutesPage path={props.match} />
</CounterContext.Provider>
);
}
SideBar.jsx
import React from 'react';
import {CounterContext} from "./LessonThemes";
import SideBarMenu from "./SideBarMenu";
import '../css/Sidebar.css'
export default function SideBar(props) {
const [BackgroundTheme, SetBackgroundTheme] = React.useContext(CounterContext);
return (
<div className="wrappers">
<nav id="sidebar" className="sidebar-wrapper modal">
<div style={{background: BackgroundTheme}} className={"sidebar-page-content"}>
<div className="sidebar-brand">
<div className="sidebar-brand-container">
<div>
{props.list}
</div>
<div>
<span href="#">Theme</span>
</div>
</div>
</div>
<div className="sidebar-menu">
<SideBarMenu path={props.match.path}/>
</div>
...
</div>
</nav>
</div>
);
}
I don't know if it will be useful to you or not, but in addition I want to demonstrate the RoutesPage component, although everything is in order there
RoutesPage.jsx
import React from "react";
import {Route, Switch} from "react-router-dom";
import '../css/Sidebar.css'
import {CounterContext} from "../components/LessonThemes";
function RoutesPage(props) {
const {path} = props.path;
const routes = [
{
path: `${path}`,
exact: true,
component: () => <h2>Home</h2>
},
{
path: `${path}/Calendar`,
component: () => <h2>Test123</h2>
},
{
path: `${path}/Guardian`,
component: () => <h2>Shoelaces</h2>
}
];
const [BackgroundTheme, SetBackgroundTheme] = React.useContext(CounterContext);
return (
<>
<main style={{background: BackgroundTheme}} className="page-content">
<div className="page-container">
<h2>Pro Sidebar</h2>
<hr/>
<div className="tabs">
<Switch>
{routes.map((route, index) => (
<Route
key={index}
path={route.path}
exact={route.exact}
component={route.component}
/>
))}
</Switch>
</div>
</div>
</main>
</>
);
}
export default RoutesPage;
Is it possible that in the SideBar.jsx, the div style needs to be set to BackgroundTheme not SideBarBackgroundTheme? Also wouldn't it be passing in the object so you would still need to key into it for specific colors? Like BackgroundTheme.side?
The problem is that the side property is never being stored to the value of the context. Your context value is still just a string. The complete theme object exists is the local state of the LessonThemes component only.
In the onChange handler of your input you call SideBarPageContent which in turn calls SetBackgroundTheme, which updates the BackgroundTheme property which you pass to the context provider. The argument that you are passing to this function call is theme.ThemeBackground -- which is the background color only and not the entire object.
You likely want to refactor your code so that the context contains the whole object.
Within the SideBar component, it's unclear where you think that the variable SideBarBackgroundTheme is coming from, but that variable doesn't exist.
LessonThemes.jsx
import React, {useState, useEffect, createContext} from "react";
import SideBar from "./SideBar";
import RoutesPage from "../pages/Routes";
export const CounterContext = createContext([]);
export default function LessonThemes(props) {
const [SideBarTheme, SetSideBarTheme] = useState(localStorage.getItem("SideBarKey"));
const [PageContentTheme, SetPageContentTheme] = useState(localStorage.getItem("PageContentKey"));
const [themes, setThemes] = useState([
{
name: "G",
SideBar: "maroon",
PageContent: "blue",
},
{
name: "R",
SideBar: "gray",
PageContent: "green",
},
])
useEffect(() => {
localStorage.setItem("SideBarKey", SideBarTheme, "PageContentKey", PageContentTheme);
})
const SideBarPageContent = (PageContent, SideBar) => {
localStorage.setItem('PageContentKey', PageContent, 'SideBarKey', SideBar);
SetPageContentTheme(PageContent);
SetSideBarTheme(SideBar);
}
const list = themes.map((theme, index) => {
return (
<label key={index}>
<input
onChange={() => SideBarPageContent(theme.PageContent, theme.SideBar)}
type="radio"
name="background"
/>{theme.name}</label>
);
})
return (
<CounterContext.Provider value={{
SideBarValue: [SideBarTheme, SetSideBarTheme],
PageContentValue: [PageContentTheme, SetPageContentTheme]
}}>
<SideBar list={list} {...props} />
<RoutesPage path={props.match}/>
</CounterContext.Provider>
);
}
SideBar.jsx
export default function SideBar(props) {
const { SideBarValue } = React.useContext(CounterContext);
const [SideBarTheme, SetSideBarTheme] = SideBarValue;
return (
<div className="wrappers">
<nav id="sidebar" className="sidebar-wrapper modal">
<div style={{background: SideBarTheme}} className={"sidebar-page-content"}>
<div className="sidebar-brand">
<div className="sidebar-brand-container">
<div>
{props.list}
</div>
<div>
<span href="#">Theme</span>
</div>
</div>
...
RoutesPage.jsx
import React from "react";
import {Route, Switch} from "react-router-dom";
import '../css/Sidebar.css'
import {CounterContext} from "../components/LessonThemes";
function RoutesPage(props) {
const {path} = props.path;
const routes = [
{
path: `${path}`,
exact: true,
component: () => <h2>Home</h2>
},
{
path: `${path}/Calendar`,
component: () => <h2>Test123</h2>
},
{
path: `${path}/Guardian`,
component: () => <h2>Shoelaces</h2>
}
];
const { PageContentValue } = React.useContext(CounterContext);
const [PageContentTheme, SetPageContentTheme] = PageContentValue;
return (
<>
<main style={{background: PageContentTheme}} className="page-content">
...
Im really rellied about this issue, the case im trying to use a Material UI slider with redux.
here the slider component:
import { Slider } from '#material-ui/core'
const RangeSlider = ({handleRange, range}) => {
const handleChange = (event, newValues) => {
handleRange(newValues)
}
return (
<Slider
value={range}
onChange={handleChange}
valueLabelDisplay="auto"
aria-labelledby="range-slider"
min={0}
max={100}
/>
)
}
export default RangeSlider
here the filter component with redux logic:
import {
Button,
Typography as Tp,
Select,
MenuItem,
Accordion,
AccordionDetails,
AccordionSummary,
} from "#material-ui/core";
import { ExpandMore } from "#material-ui/icons";
import { RangeSlider } from "../components";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { filterUpdate } from "../redux/actions/filter";
const Filter = ({ models }) => {
const [range, setRange] = useState([0,60]);
const [modelSelected, setModelSelected] = useState("None");
const handleRange = (newValue) => setRange(newValue);
const handleModel = (event) => setModelSelected(event.target.value);
const dispatch = useDispatch();
const handleSubmit = () => {
let filterValues = {
range,
modelSelected,
};
dispatch(filterUpdate(filterValues));
};
return (
<div className="container mt-3 ">
<Accordion>
<AccordionSummary
expandIcon={<ExpandMore />}
aria-controls="panel-filter-content"
id="panel-filter-header"
>
<Tp>Sort your search</Tp>
</AccordionSummary>
<AccordionDetails className='d-flex align-items-center bg-light pb-3'>
<div className='col-md-6'>
<Tp className="mt-3">Pick up a price range</Tp>
<RangeSlider handleRange={handleRange} range={range} />
</div>
<div className="col">
<div className="col text-center pb-3">
<Tp className="mt-3">Sort by model</Tp>
<Select value={modelSelected} onChange={handleModel}>
{models.map((model) => {
return (
<MenuItem key={model} value={model}>
{model}
</MenuItem>
);
})}
</Select>
</div>
</div>
<div className=" col mt-5 text-center">
<Button onClick={handleSubmit} variant="outlined" color="primary">
Submit
</Button>
</div>
</AccordionDetails>
</Accordion>
</div>
);
};
export default Filter;
Redux action logic:
import { createAction } from '#reduxjs/toolkit'
import types from '../types'
export const filterUpdate = createAction(types.FILTER_UPDATE)
Here Reducer:
import { createReducer } from '#reduxjs/toolkit'
import { filterUpdate } from '../actions/filter'
const reducer = createReducer({
range: [0,60],
model: 'None'
}, {
[filterUpdate]: (state, action) => {
state.range = action.payload.range
state.model = action.payload.modelSelected
}
})
export default reducer
The error appear when i include the redux logic, when i tested with a console.log was fine, but with the dispatch, appear this error, also, its a random error because i can test to dispatch 2-3 times without error, but the next this error appear
What can i do to deal with that?
Thanks in advance.
In my case, it fails to sort function inside the Slider.
That because the value that I passed into the component was a read-only (immutable).
So to avoid this error I should to pass in the value prop the copy of range array. Like this:
<Slider
value={[...range]}
// ....other props
/>
I am not sure what is the problem, but your reducer is definitely wrong. You do:
const reducer = createReducer({
range: [0,60],
model: 'None'
}, {
[filterUpdate]: (state, action) => {
state.range = action.payload.range
state.model = action.payload.modelSelected
}
})
You probably meant to do this:
const reducer = createReducer({
range: [0,60],
model: 'None'
}, {
[filterUpdate]: (state, action) => ({
range: action.payload.range,
model: action.payload.modelSelected,
})
})
Also you are not actually pulling state from redux store. You are importing useSelector and not calling it anywhere. Whatever useState and useDispatch return are independent from each other in your code.