I'm currently building a React application with the following workflow:
List of application categories, the user will select one
Once a user has selected an application category, they will select a water type from a list
A list of products will then be displayed depending on the category and type selected.
They can then select a product to see the information i.e. product charts, images etc.
The problem:
Once a user selects a product, if they click the back button, the category and type props are lost.
Solution required:
I need to be able to maintain these props/state at all times, allowing them to be updated if the user goes back and changes category/type
I've included my code for reference below.
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
ReactDOM.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
document.getElementById("root")
);
App.js
import React from "react";
import { Route, Switch } from "react-router-dom";
import Home from "./components/Home";
import WaterType from "./components/WaterType";
import Products from "./components/Products";
import Product from "./components/Product";
import "./App.css";
function App() {
return (
<div className="App">
<Switch>
<Route exact path="/" component={Home} />
<Route path="/waterType" component={WaterType} />
<Route path="/products/:productName" component={Product} />
<Route path="/products" component={Products} />
</Switch>
</div>
);
}
export default App;
Home.js
import React, { Component } from "react";
import { Link } from "react-router-dom";
import CategoryData from "./data/CategoryData";
class Home extends Component {
render() {
return (
<>
<h1>Categories</h1>
<ul>
{CategoryData.map((cat, i) => (
<li key={i}>
<Link
to={{
pathname: "/waterType",
name: cat.name,
}}
>
<img src={cat.imageURL} alt={cat.name} />
{cat.name}
</Link>
</li>
))}
</ul>
</>
);
}
}
export default Home;
WaterType.js
import React from "react";
import { Link } from "react-router-dom";
import WaterTypeData from "./data/WaterTypeData";
const WaterType = ({ location }) => {
const categorySelected = location.name;
return (
<>
<h1>Water Types</h1>
<p>Current category: {categorySelected}</p>
<ul>
{WaterTypeData.map((type, i) => (
<li key={i}>
<Link
to={{
pathname: "/products",
categorySelected: categorySelected,
waterType: type.name,
}}
>
{type.name} - {type.description}
</Link>
</li>
))}
</ul>
</>
);
};
export default WaterType;
Products.js
import React from "react";
import { Link } from "react-router-dom";
import ProductData from "./data/ProductData";
const Products = ({ location }) => {
const categorySelected = location.categorySelected;
const waterType = location.waterType;
const ProductsResult = ProductData.filter(x => x.categories.includes(categorySelected) && x.waterTypes.includes(waterType));
return (
<>
<h1>Products</h1>
<p>Current category: {categorySelected && categorySelected}</p>
<p>Water Type: {waterType && waterType}</p>
<div className="products">
<ul>
{ProductsResult.map((item, i) => (
<li key={i}>
<Link
to={{
pathname: '/products/' + item.slug,
name: item.name,
}}
>
{item.name}
</Link>
</li>
))}
</ul>
</div>
</>
);
};
export default Products;
Product.js
import React from "react";
const Product = ({ location }) => {
const productName = location.name;
return (
<>
<h1>{productName}</h1>
</>
);
};
export default Product;
The easiest solution that I can think of is to keep your selected choices (category and water type) in a top level context.
Something like this:
// ChoicesProvider.js
import React, { createContext, useState } from "react";
export const ChoicesContext = createContext(null);
export const ChoicesProvider = ({ children }) => {
const [choices, setChoices] = useState({
category: null,
waterType: null,
});
return (
<ChoicesContext.Provider value={{ choices, setChoices }}>
{children}
</ChoicesContext.Provider>
);
};
…and then in your entry point:
// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter as Router } from "react-router-dom";
import { ChoicesProvider } from "./context/ChoicesProvider";
ReactDOM.render(
<React.StrictMode>
<ChoicesProvider>
<Router>
<App />
</Router>
</ChoicesProvider>
</React.StrictMode>,
document.getElementById("root")
);
…and then each time you pick a category / waterType save the selected state in a context using setChoices defined in context. For example:
// Home.js
import React, { Component } from "react";
import { Link } from "react-router-dom";
import CategoryData from "./data/CategoryData";
import { ChoicesContext } from "../../context/ChoicesContext";
const Home = () => {
const { choices, setChoices } = useContext(ChoicesContext);
return (
<>
<h1>Categories</h1>
<ul>
{CategoryData.map((cat, i) => (
<li key={i}>
<Link
onClick={() => setChoices({ ...choices, category: cat.name })}
to={{
pathname: "/waterType",
name: cat.name,
}}
>
<img src={cat.imageURL} alt={cat.name} />
{cat.name}
</Link>
</li>
))}
</ul>
</>
);
};
export default Home;
Hopefully that gives you an idea.
Have a great day 👋
First of all, I am not sure if your Router setup is necessary. React is great for single page applications (SPA), this means, you don't neet new page for every single functionality. You can very comfortably build your application on single Route, changing just the components you currently need.
So the first possible solution is to build the application on single page, using three simple states
[category, setCategory] = useState(null)
[waterType, setWatertype] = useState(null)
[product, setProduct] = useState(null)
Based on this you can simply show or hide the select options
<div id="selections">
{!category && (<Categories />)}
{(category && !watertype) && (<WaterTypes />)}
{category && watertype) && (<Products />)}
</div>
category && <WaterTypes /> means, WaterTypes will be displayed only if category is true.
This if course requires your Link to be replaced with something like
<button type="button" onClick={() => setCategory(cat.name)}>{cat.name}</button>
With this approach you can then handle back and forward button to manipulate your states and URL, so the user has access to desired category with hyperlink like www.youreshop.com?category=first&watertype=second
Another approach is to add context to your app, what would allow you to share states between individual components.
I highly recommend Redux (https://redux.js.org/) for application state managemt.
In general it's a good idea to include Redux to modern React applications, and in your case it seems to be exactly what you're looking for.
Hope this helps.
Cheers
Related
I'm studying React and trying to make a Todo app. I made a first section and a menu which should switch the app between two sections. I'm mapping buttons in order to add them class because they should have different styling after they are clicked. Now I need to add routing to these buttons but I can't make it work.
This is my code:
import React, { useState } from "react";
import Todo from "../Todo/Todo";
import About from "../About/About";
import { BrowserRouter as Router, Route, Link, Routes } from "react-router-dom";
import classnames from "classnames";
import styles from "./App.module.css";
const App = () => {
const [clicked, setClicked] = useState("");
const buttons = ["About me", 'Todos'];
return (
<Router>
<div>
<nav className={styles.menu}>
<ul className={styles.nav}>
{buttons.map((item, index) => {
return (
<li
key={index}
onClick={() => {
if (clicked === index) {
setClicked();
} else {
setClicked(index);
}
}}
>
<Link to={`/buttons/${index}`}>
{
<button
className={classnames({ [styles.button]: true, [styles.selected]: clicked === index
})}
>
{item}
</button>
}
</Link>
</li>
);
})}
</ul>
</nav>
<Routes>
<Route path="/" exact element={<About />} />
<Route path="/todo" element={<Todo />} />
</Routes>
</div>
</Router>
);`
export default App;
The error message I get in console is 'No routes matched location "/buttons/0"'. And the same for "/buttons/1".
Probably I indicated paths wrong somehow but I don't know how to fix it.
I'm new in React and doesn't know how to create List and Detail view using React Router DOM. Some how I created it but I know this is not a good way to do it. If you look into below code, you will find it will always reload the whole page if I select another campaign. What will be the good way to do it. Please help...
App.js
<Switch>
<Fragment>
<PrivateRoute exact path="/admin/campaigns/:id" component={CampaignComponent}/>
<PrivateRoute exact path="/admin/campaigns/:id/messages" component={CampaignComponent}/>
<PrivateRoute exact path="/admin/campaigns/:id/contacts" component={CampaignComponent}/>
</Fragment>
</Switch>
Campaign.js
let layout;
switch (props.match.path) {
case '/admin/campaigns/:id':
layout = <CampaignDetailComponent props={props}/>;
break;
case '/admin/campaigns/:id/messages':
layout = <CampaignMessageListComponent props={props}/>;
break;
case '/admin/campaigns/:id/contacts':
layout = <CampaignContactListComponent props={props}/>;
break;
default:
layout = <div/>;
}
return (
<div>
<div className="col-6">
<CampaignListComponent props={props}/>
</div>
<div className="col-6">
{layout}
</div>
</div>
)
So, in the first col-6 I want to show the list of Campaigns and in second col-6 I want to render the components as per route change.
You can follow this link to see the actual working code sample that demonstrate this issue.
Issue
You have your "navigation" coupled to your page UI.
Solution
Split out the rendering of your links in the left section from the content in the right section. The rest of the changes a centered around computing paths and route structuring.
App.js
Render the campaign list container and navigation on its own route. This is so the nested links can inherit from the route path.
import React, { Component } from "react";
import { HashRouter, Redirect, Route, Switch } from "react-router-dom";
import CampaignComponent from "./Campaign/Campaign";
import CampaignListComponent from "./Campaign/Components/CampaignList";
import "./styles.css";
class App extends Component {
render() {
return (
<div className="App">
<h3>Sample app to demonstrate React Router issue</h3>
<HashRouter>
<div className="campaign-container">
<div className="campaign-list">
<Route path="/admin/campaigns">
<CampaignListComponent />
</Route>
</div>
<div className="campaign-detail">
<Switch>
<Route path="/admin/campaigns" component={CampaignComponent} />
<Redirect from="*" to="/admin/campaigns" />
</Switch>
</div>
</div>
</HashRouter>
</div>
);
}
}
Campaign list component
import React, { useState } from "react";
import { Link, generatePath, useRouteMatch } from "react-router-dom";
const CampaignListComponent = () => {
const [campaignList, setCampaignList] = useState([
{ id: 1, name: "Campaign1" },
{ id: 2, name: "Campaign2" },
{ id: 3, name: "Campaign3" },
{ id: 4, name: "Campaign4" },
{ id: 5, name: "Campaign5" }
]);
const { url } = useRouteMatch();
return (
<div style={{ width: "90%" }}>
{campaignList.map(({ id, name }) => (
<div className="campaign-list-item" key={id}>
<Link to={generatePath(`${url}/:id`, { id })}>{name}</Link>
</div>
))}
</div>
);
};
Campaign component
import React from "react";
import { Route, Switch, useRouteMatch } from "react-router-dom";
import CampaignMessageListComponent from "./Components/CampaignMessageList";
import CampaignDetailComponent from "./Components/CampaignDetail";
import CampaignContactListComponent from "./Components/CampaignContactList";
const CampaignComponent = () => {
const { path } = useRouteMatch();
return (
<div className="campaign-list">
<Switch>
<Route
path={`${path}/:id/messages`}
component={CampaignMessageListComponent}
/>
<Route
path={`${path}/:id/contacts`}
component={CampaignContactListComponent}
/>
<Route path={`${path}/:id`} component={CampaignDetailComponent} />
</Switch>
</div>
);
};
Content components
Contact
import React from "react";
import { useHistory } from "react-router-dom";
const CampaignContactListComponent = () => {
const history = useHistory();
return (
<div className="row">
<p>Campaign Contact List</p>
<button onClick={history.goBack}>Go Back</button>
</div>
);
};
Message
import React from "react";
import { useHistory } from "react-router-dom";
const CampaignMessageListComponent = () => {
const history = useHistory();
return (
<div className="row">
<p>Campaign Message List</p>
<button onClick={history.goBack}>Go Back</button>
</div>
);
};
Detail
import React from "react";
import { Link, useHistory, useParams, useRouteMatch } from "react-router-dom";
const CampaignDetailComponent = () => {
const { id } = useParams();
const { url } = useRouteMatch();
const history = useHistory();
return (
<div>
<h6>Campaign Detail</h6>
<p>You have selected Campaign - {id}</p>
<button>
<Link to={`${url}/messages`}>Goto Messages</Link>
</button>
<button>
<Link to={`${url}/contacts`}>Goto Contacts</Link>
</button>
<button onClick={history.goBack}>Go Back</button>
</div>
);
};
'When i visit "/shop/hats" it shows only blank page. No error i could see. But when i put category component in 1st route, it works.'
'shop component'
import React from "react";
import CollectionPreview from "../../component/CollectionPreview/collectionPreview-com"
import {Route} from "react-router-dom"
import Category from "../../component/Category/category-com"
const Shop = ({match}) => {
console.log("Printing" + match.path);
return(
<div>
<Route exact path="/shop" component={CollectionPreview} />
<Route path="/shop/:categoryId" component={Category} />
</div>
)
}
export default Shop;
'Category component which is not rendering. but when i use this component in 1st route, it works '
import React from "react";
import "./category-style.css";
const Category = ({match}) => {
console.log(match);
return(
<div className="Test">
<h1>Test</h1>
</div>
)
}
export default Category;
'I even tried mentioning 2nd route as normal route (Without Param-route). No luck'
Try this:
import { Switch, Route } from 'react-router-dom';
//your code
return (
<Switch>
<Route exact path="/shop" component={CollectionPreview} />
<Route path="/shop/:categoryId" component={Category} />
</Switch>
);
For more details about when to use Switch, refer this: here
you can try with useParams() hooks. like below
import React from "react";
import { useParams } from 'react-router-dom';
import "./category-style.css";
const Category = () => {
const {categoryId} = useParams();
return(
<div className="Test">
<h1>Test</h1>
</div>
)
}
export default Category;
I'm currently coding my portfolio website. Inside each project page, there is a section above the footer that is basically a nav to endorse other projects.
I'm new to react, and I was wondering if there was a way to find the current URL (just the extension) and exclude that from the projects being displayed?
All my project data (link, title, name, description) are organized in an array, so theoretically it would be
if (projectData.link != currentURL){
// pass the props
}
The MoreProjectCard is a component, code is below:
function MoreProjects() {
const moreProjects = projectData.map((project) => (
<MoreProjectCard
id={project.name}
key={project.name}
link={project.link}
title={project.title}
description={project.description}
/>
));
return (
<div className="section padding-h">
<h3 className="mb-50">View other projects</h3>
{moreProjects}
</div>
);
}
I can provide more code if needed.
Thank you!
Update:
projectData:
export default [
{
name: 'doodlevr',
title: '50 Years of VR',
link: '/doodlevr',
category: 'AR/VR, Front End Development',
description: 'Reinventing the Google Doodle',
},
{
name: 'bridge',
title: 'Bridge',
link: '/bridge',
category: 'UX/UI Design, User Research',
description: 'Fostering connections between students and mentors',
},
{
name: 'stellar',
title: 'Stellar',
link: '/stellar',
category: 'UX/UI Design',
description: 'Empowering families through storytelling',
},
];
an example URL would be www.roneilla.com/stellar
I would be looking at filtering out "/stellar"
below is the code for the moreProjectCard component
import React from 'react';
import { Link } from 'react-router-dom';
import { ReactComponent as Arrow } from './../assets/arrow.svg';
import './../Text.css';
import './../App.css';
function MoreProjectCard(props) {
return (
<div className="moreProjectCard padding-h" id={props.id}>
<Link to={props.link}>
<h4>{props.title}</h4>
<p>{props.description}</p>
</Link>
<Arrow className="more-arrow-icon" />
</div>
);
}
export default MoreProjectCard;
Update
import React from 'react';
import { BrowserRouter, Route, Link } from 'react-router-dom';
import { ReactComponent as Logo } from './assets/logo.svg';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import ScrollToTop from './ScrollToTop';
import CaseStudies from './pages/CaseStudies';
import About from './pages/About';
import Doodlevr from './pages/Doodlevr';
import './App.css';
function App() {
return (
<BrowserRouter>
<ScrollToTop />
<div className="app">
<Navbar />
<Route exact path="/" component={CaseStudies} />
<Route exact path="/doodlevr" component={Doodlevr} />
<Route exact path="/about" component={About} />
<Footer />
</div>
</BrowserRouter>
);
}
export default App;
If I understand your app structure, then each route renders a page component with a MoreProjects component at the bottom of the page. This means MoreProjects is being rendered within a Route and has access to the route-props. The route location pathname is what you need for comparisons.
Since MoreProjects is a functional component you can use the useLocation hook to get this information.
So given URL www.roneilla.com/stellar, the current matching location.pathname is /steller.
function MoreProjects() {
const { pathname } = useLocation();
const moreProjects = projectData
.filter(({ link }) => link !== pathname) // <-- filter the link by current pathname
.map((project) => (
<MoreProjectCard
id={project.name}
key={project.name}
link={project.link}
title={project.title}
description={project.description}
/>
));
return (
<div className="section padding-h">
<h3 className="mb-50">View other projects</h3>
{moreProjects}
</div>
);
}
I have multiple components I am trying to route but I will only show a few components here so I don't overwhelm you. I have installed and imported everything correctly I think.
Here is App.js that controls all the routes, the component "Hello" is for testing, there are no props involved or anything and its still not showing in the DOM. Does it have anything to do with not passing props because before using the router to separate my components, I would just do for example: Are these not being automatically passed with router? I am a beginner using React so any help would be great, thank you!
import React, { Component } from 'react'
import Clients from './components/Clients'
import Lessons from './components/Lessons'
import AddClient from './components/AddClient'
import NavBar from './components/NavBar'
import Hello from './components/Hello'
import AddLesson from './components/AddLesson'
import CalculateIncome from './components/CalculateIncome'
import {Switch, BrowserRouter, Route} from 'react-router-dom'
class App extends Component {
state = {
clients : [],
lessons : [],
}
deleteClient = (id) => {
let clients = this.state.clients.filter(client => {
return client.id !== id
})
this.setState({
clients: clients
})
}
addClient = (newClient) => {
newClient.id = Math.random();
let clients = [...this.state.clients, newClient];
clients.sort((a, b) => a.name.localeCompare(b.name))
this.setState({
clients: clients,
})
}
addLesson = (newLesson) => {
newLesson.id = Math.random();
newLesson.date = Date();
let lessons = [...this.state.lessons, newLesson];
this.setState({
lessons: lessons,
})
console.log(this.state.lessons)
}
render() {
return (
<BrowserRouter>
<div className="App">
<NavBar/>
<Switch>
<Route exact path='/' Component={AddLesson}/>
<CalculateIncome lessons={this.state.lessons} clients={this.state.clients}/>
<Route path='/addClient' Component={AddClient}/>
<Route path='/clients' Component={Clients}/>
<Route path='/lessons' Component={Lessons}/>
<Route path='/hello' Component={Hello}/>
</Switch>
</div>
</BrowserRouter>
);
}
}
export default App;
Here is my navbar.js
import React from 'react'
import {NavLink, withRouter} from 'react-router-dom'
const NavBar = (props) => {
return (
<nav>
<div>
<h1 className="header text-center my-5" >Welcome to the Tennis App</h1>
<div className="container text-center">
<NavLink className="mx-5" exact to="/">Add Lesson</NavLink>
<NavLink className="mx-5" to='/addClient'>Add a New Client</NavLink>
<NavLink className="mx-5" to="/clients">Clients</NavLink>
<NavLink className="mx-5" to="/lessons">Lessons</NavLink>
<NavLink className="mx-5" to="/hello">Hello</NavLink>
</div>
</div>
</nav>
)
}
export default withRouter(NavBar)
And here is hello.js that is just a dummy test component
import React from 'react'
const Hello = () => {
return(
<div className="text-center">Hello there</div>
)
}
export default Hello
I realized I have to use the render={Home} instead of Component={Home}
Here is a link to the answer I found after extensive 2 day research...
https://medium.com/alturasoluciones/how-to-pass-props-to-routes-components-29f5443eee94