Adding multiple params with react-router-dom and issue with concatenation - javascript

First, I've tried fixing this issue using this: Multiple params with React Router. But there's def. things I do not get in this context. I'm fairly new to react-router-dom and I'm trying to have two params in the URL, like this: /:type/:projectid.
I have instantiated my <Link> like this, coming from an array of data:
let projectRoutePlaceholder = `${ele.type}/project${this.props.index}`
return(
<Router>
<Link to={projectRoutePlaceholder}>
....
</Link>
</Router>
);
When I hover on my content, I therefore have the route URL displaying on the bottom left of the browser. On my index.js file, I've set up my routes like this:
ReactDOM.render(
<React.StrictMode>
<Router>
<Route exact path='/' component={App} />
<Route exact path='/index' component={ProjectIndex} />
<Route exact path='/info' component={Info} />
<Route exact path='/:type/:id' component={ProjectTemplate} />
</Router>
</React.StrictMode>,
document.getElementById('root')
Now, two issues I'm not able to fix.
When I click on a <Link> element, it does not send me to the ProjectTemplate component and it stays on the <Link> element. When I force (refresh the page with the exact route, e.g: commercial/project0), it works.
When I click once again on the <Link> element, params gets concatenated (e.g: commercial/commercial/project0)
How can I fix these two issues?

With react-router-dom you only want to have 1 top level Router that wraps around your routes. Your Links would also have to come at some point in the child tree of that 1 router. The outer Router holds the context of your current route and causes state updates which is how your different routes come in to effect. Because you are wrapping every single Link with it's own Router, when you click on one of those Links, the Router wrapping them is the only one getting updated which doesn't have any child routes to update/show. If you get rid of these wrapping Routers from your Links and just keep the top level one around your Routes, this should fix the issues you are running in to.
Change let projectRoutePlaceholder = `${ele.type}/project${this.props.index} to let projectRoutePlaceholder = `/${ele.type}/project${this.props.index} so that it has a leading slash at the beginning. If you don't put a leading slash, then react-router-dom will assume you want to append the url to the end of your current route which will cause the concatenation issue you're running in to. By adding a leading slash, you are telling react-router-dom to go directly to the route rather than append it.

Related

Struggling to lift state up- is this a structural issue or am I missing something obvious?

I'm making my first React front end and I'm struggling a bit with passing information between components. I'm not sure if I'm missing something obvious or if the way I've mapped things out is fundamentally flawed. Since this is my first time really messing with React, I'm trying to avoid hooks if I can, so everything's class-based.
Here's a screenshot of what the page looks like right now: https://ibb.co/xDm0dyp
And a stupid chart representing the components (and the state I want to move): https://ibb.co/7WB2T4z
Each of those t-shirt cards is generated as you scroll down the page, each with a quote nabbed from an API.
As I illustrate in my beautiful chart, I want that button in my t-shirt cards to send you to a different view where you can select size, color, etc. for the shirt. The important thing is that each of those t-shirt cards contains a quote as a prop, and the components further up don't ultimately know what quote each card contains. Since this "Details" view is going to replace the whole "Home" view, I don't think it can be a child of a generated card (can it?) so I'm not sure how to get that quote to it.
Currently I have a Switch in my App component:
<Switch>
<Route exact path='/' render={() => <Home />}/>
<Route exact path='/shirtselect' render={() => <Shirtselect/>}/>
</Switch>
shirtselect is my view for card details, and I can't just pass a quote as a prop in the tag there, because the quote is sitting two layers down in a card component generated as the page is scrolled. I can get the quotes into the Home view just fine.
​
Each of those t-shirt cards is generated by the Home component with this code:
render() {
return (
<section id='market'>
{this.state.quotes.map(quote => (<ShirtCard quote = {quote} addToCart = {this.props.addToCart} />))}
<div ref={this.bottom} > </div>
</section>
)
}
​
And the cards themselves include this link (LinkContainer is from react-router-bootstrap, just think of it as a tag):
<LinkContainer to='/shirtselect' state={{quote: this.props.quote}}>
<Button
size='lg'
variant='dark'>
<i className="bi bi-bag-plus"></i>
</Button>
</LinkContainer>
I currently include that state prop on the advice of this article, but ultimately that would require use of the useLocation() hook, and I've been trying to avoid using hooks until now.

React Router Tabs -- Keep Components Mounted

I have created tabs using React Router, with a different route for each tab. However, I would like to maintain the tab state between tab transitions by keeping the hidden tabs mounted. How do I achieve this? React router remounts each component every time the route switches.
Someone has already asked this question here, but has not received an answer
Ideally I would find a solution which keeps the tabs which are not displayed mounted after they are hit for the first time
I'd have to do a little more digging to confirm this actually works, but reading through React Router docs I found this about the Route component. Using the component prop makes the component remount every time the route changes. But using the other render methods, you might be able to achieve what you're looking for. I'd go with render, but children might work too?
This is the recommended method of routing by react-router-dom-v5 doc over render,children and component prop of <Route/>. This way our component gets re-initialized & re-mounted everytime path is matched.
<Switch>
<Route exact path="/">
<Home />
</Route>
<Route path="/contact">
<Contact />
Route>
<Route path="/about">
<About />
</Route>
</Switch>
As you(Kat) want to maintain the tab state between tab transitions by keeping the hidden tabs mounted.
you can achieve this by mounting all the tabs at once and then switch between the tabs by using react-router-dom's pathname.
const { pathname } = useLocation();
{pathname === "/"? <Home/>: null}
{pathname === "/contact"? <Contact/>: null}
{pathname === "/about"? <About/>: null}
This way your component will not get re-initialized and re-mounted everytime path is matched and hence component states will not be disturbed and will be taken care of automatically accross the tabs.
Here is the working DEMO: https://codesandbox.io/s/react-router-experiment-ilfhq?file=/src/component/Home.js:166-201
Hope I answered your question.
Here is the second solution DEMO using CSS: https://codesandbox.io/s/react-router-mouted-routes-32dxf?file=/src/App.js

Update page contents based on URL and do not reload the whole page

Context
I am using ReactRouter 4.
I have the following base Router
<Router>
<Route exact path="/" component={HomePage} />
<Route
path="/courses/:courseName"
render={(props) => {
switch (props.match.params) {
case '1':
return React.createElement(
fetch('courses/1/')(CoursePage),
props,
null
);
case '2':
return React.createElement(
fetch('courses/2/')(CoursePage),
props,
null
);
default:
return <FourOFour />
}
}}
/>
</Router>
What fetch('courses/NUMBER/')(CoursePage) does, is that it wraps the CoursePage component inside a HOC which makes an API request to fetch data relevant to the specific course and when it gets a successful response the CoursePage component gets rendered with the data passed as props. In the meanwhile a loading screen is displayed instead.
Now, inside CoursePage the way its contents are laid out is based on whether the user is logged in or logged out.
If the user is logged out, the LoggedOut component is rendered inside the CoursePage's render() method, and they see one page with all content laid continuously in the page.
If the user is logged in, the LoggedIn component is rendered inside the CoursePage's render() method, and they see the same content as above, but it is now broken into tabbed sections, meaning the user can no longer see the whole content of the page with one scroll, they have to select the appropriate tab in order to see the relevant content.
So, for example, if I was logged out, and hit the page for course 1, I would go to www.page.com/courses/1 and see a page with 3 sections (Overview, Contents, Reviews) one after the other.
If I was logged in and hit the page for course 1, I would go to www.page.com/courses/1 and see a page with 3 tabs (Overview, Contents, Reviews) and clicking each tab should display the relevant content.
Now, I got the following requirement:
Each section, when the user is logged in, should reflect on the URL when it's selected. So, if I click on the "Contents" tab, while logged in, the URL should become www.page.com/courses/1/contents.
Problem
I decided to implement this functionality with ReactRouter, so, in the LoggedIn component's render() method, I made the tabs NavLink elements, and I placed the following code:
<Router>
<section>
<Route
exact
path="/courses/:courseName"
render={(props) => {
return this.determineVisibleSection('overview', data);
}}
/>
<Route
path="/courses/:courseName/:section"
render={(props) => {
return this.determineVisibleSection(props.match.params.section, data);
}}
/>
</section>
</Router>
this.determineVisibleSection(sectionName, data) simply passes the data to the appropriate component to render based on the sectionName and returns it.
The problem with this, is that when a section is clicked, the whole page loads again.
What I mean by that, is that the fetch('courses/NUMBER/')(CoursePage) is fired again, and we get the loading screen while we wait for the data to return, and finally the page is displayed with the section that we clicked now selected and the correct content below.
I think I understand why this happens. Since the URL is changed, all Router components get notified of the change and so do their Route components, so since in the base Router the component to render is basically new each time, even if we are on the same page, since a new one is returned by fetch()(), the page is "reloaded".
The question
Is there any way to prevent this behaviour? Meaning, to not have the change to the URL, by the selected section, affect the whole page. Have it only affect the current course page contents.
One way I came up with, that seems to work is the following:
I rewrote my base Router like this
<Router>
<Page>
<Route exact path="/" component={HomePage} />
<Route path="/courses/:courseName" component={CoursePages} />
</Page>
</Router>
and CoursePages is this
class CoursePages extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.match.url !== nextProps.match.url;
}
render() {
switch (this.props.match.url) {
case '/courses/1':
return React.createElement(fetch('courses/1/')(CoursePage), this.props, null);
case '/courses/2':
return React.createElement(fetch('courses/2/')(CoursePage), this.props, null);
default:
return <FourOFour />;
}
}
}
The key to this whole thing being the shouldComponentUpdate and the fact that I have the second <Router /> in the LoggedIn component, since the second <Router /> will force its contents to re-render even if the parent component does not re-render (because of the shouldComponentUpdate)
Is there a better way to do that I am missing?
I think putting the section routes inside the LoggedIn component is better. Because it's more react-router v4 way to handle this kind of scenarios (see this basic example from the documentation).
First, keep your base Router in the first code block as it is. With the given explanation, I assume your CoursePage component is more or less like this.
class CoursePage extends Component{
//...
render(){
return this.props.IsLoggedIn ? <LoggedIn/> : <LoggedOut/>
}
}
And now put Routes in your LoggedIn component for different sections as follows.
import React, { Component } from 'react';
import {
Switch
Route,
Redirect
} from 'react-router-dom'
class LoggedIn extends Component{
// ...
render(){
const { match } = this.props;
return (
<Switch>
{/* This will redirect /courses/:courseName to /courses/:courseName/overview */}
<Redirect exact from={`${match.url}/`} to={`${match.url}/overview`}/>
<Route path={`${match.url}/overview`} component={Overview}/>
<Route path={`${match.url}/contents`} component={Contents}/>
<Route path={`${match.url}/reviews`} component={Reviews}/>
</Switch>
);
}
}
Probably you will need to add more component or HTML tags to this render method to make it looks like tabs with headers.
Since section routes are rendered after CoursePage get rendered, it won't fetch data again when you navigate to different sections in the same course.

react link vs a tag and arrow function

I just started on react router.
I have two questions. What is the difference between using <Link to="/page"> and <a href="page">? Both make the exact same get request to /page but I get an error when I use <a href="page"> but it works when I use <Link to="/page"> when I am nesting routes. I don't understand, how there could be any difference, when I know for fact that both render to exact same url?
Second is the weird arrow function in react router v4 documentation
const About = () => (
<div>
<h2>About</h2>
</div>
)
I know () => {} these are new in ES6 but I cannot find anything on normal brackets instead of parentheses. What are they?
Edit
My index.js class (I have all the imports)
render((
<Router>
<div>
<Route component={App}/>
</div>
</Router>
), document.getElementById('root')
);
My App.js class
class App extends Component {
render() {
return (
<div className="container">
<header>
<span className="icn-logo"><i className="material-icons">code</i></span>
<ul className="main-nav">
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/teachers">Teachers</Link></li>
<li><Link to="/courses">Courses</Link></li>
</ul>
</header>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/teachers" component={Teachers}/>
<Route path="/courses" component={Course}/>
</div>
);
}
}
export default App;
The error I'm getting.
Cannot GET /about on the browser when I try to move to localhost:8080/about. However, when I click the about button, it goes to exactly the same url /about and renders perfectly
This may be a bit late to address your issue and you may well have figured it out. But here's my take:
First:
What is the difference between using <Link to="/page"> and <a
href="page">
On the surface, you seem to be comparing apples and oranges here. The path in your anchor tag is a relative path while that one in the Link is absolute (rightly so, I don't think react-router supports relative paths yet). The problem this creates is say you are on /blah, while clicking on your Link will go to /page, clicking on the <a href='page' /> will take you to /blah/page. This may not be an issue though since you confirmed the correctness of the url, but thought to note.
A bit deeper difference, which is just an addon to #Dennis answer (and the docs he pointed to), is when you are already in a route that matches what the Link points to. Say we are currently on /page and the Link points to /page or even /page/:id, this won't trigger a full page refresh while an <a /> tag naturally will. See issue on Github.
A fix I used to solve my little need around this was to pass in a state property into link like so <Link to={{pathname: "/page", state: "desiredState"}}>Page</Link>. Then I can check for this in the target component's (say <Page />) componentWillReceiveProps like so:
componentWillReceiveProps(nextProps){
if (nextProps.location.state === 'desiredState') {
// do stuffs
}
}
Second question:
the weird arrow function in react router v4 documentation... I cannot find anything on normal brackets instead of parentheses. What are they?
Arrow functions; again #Dennis and #Jaromanda X have kind of addressed it. However, I've got three bits to add:
When you have () => blah without the curly braces {}, you are implicitly returning whatever follows the => in this case blah. But when you have curly braces immediately after the arrow, then it's now your responsibility to return something if you so desire. So () => blah (which by the way is synonymous to () => (blah)) will be more similar to () => { return blah } and not () => { blah }.
So what happens if you want to return an object: { blah: blah }; this is what #Jaromanda X was pointing at. You will then need to do () => ({ blah: blah }) or simply () => ({ blah }) for implicit return or you could return explicitly like so () => { return { blah: blah } }.
My third bit is to point you to MDN
Hope it helps.
The href attribute would trigger a page refresh which would reset the application states. However the link and navlink of react-router doesn't trigger a page refresh. Since React is used to create single page applications most of the time make sure you choose Link or Navlink when working with routing
The component allows you to do more than the normal link element. For instance, because it's a React component you have the benefits of having a state and what not (if you want that). You can see more documentation on here. Without the error I'm not sure what happens, but I suspect the routing library wants you to use the component, over a normal html element.
With regards to () => {} this is a construct which is called an anonymous function, or a lambda expression. It's basically the same as saving a function in a variable: var x = function(){ return (<div>...) }; if you have anything in the first parenthesis, it's a parameter which you have access to: const x = (y) => return y*2; The reason it's done in React is to expose the function scope to the component it lies in.
There is no better then looking at the code source.
https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js
You can see that Link is a component, that internally use history. Which is the module|library behind the history and navigation for react-router. And come with different modes (in memory history, browserHistory, hashHistory. And even custom).
Yea as a similarity it render an anchor tag but the default behavior is overridden (preventDefault()). They could have used just a div. But not completely right. As for the reason bellow.
So basically it work like that:
Observe the condition bellow
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
(!this.props.target || this.props.target === "_self") && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
}
if the condition above is met. It will use history (push or replace). Otherwise it will leave the browser normal behavior. And so in that case it will be just a normal anchor tag <a />. Example letting the browser handle target='blank'. The condition are well explained.
Then depending on the type of history object. The behavior change. Not the behavior of ` itself. But just the result of the history object type.
In resume:
<Link /> is a component, that render a <a /> anchor tag. However in the main conditions the default behavior is prevented (preventDefault()). That allow it to apply the change to the history object (onClick event). Which react-router navigation is based on. And on the some conditions as mentioned above. It just fall back to the browser behavior. And just be exactly a <a /> anchor tag (no preventDefault()).
For the use. If you are using React-router. Then you just need to use Link.

What is a good way to make a link between two React components at different hierarchy levels?

I'm relatively new to React and I'm trying to figure out how I should compose a complex application (not just a simple TODO app).
I have basically a structure like this (greatly simplified):
<Application>
<MenuBar />
<Router>
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
<Route path="/page3" component={Page3} />
</Router>
</Application>
<MenuBar> is basically an AppBar, however with the left icon not visible at all times.
There will be many Page components (visible below the MenuBar) and a few of them will use a Drawer for varying reasons.
Depending on the available screen resolution I want my application to be responsive and either:
use the Drawer component on small screens, or
show a fixed sidebar on large screens (like the opened Drawer, but without covering the main content)
This screenshot makes it easier to understand, perhaps:
The content of the Drawer and the sidebar will be exactly the same, as only either one is visible.
Therefore, I'd like to create a <DynamicDrawer> component that can be used at the top level of any Page component:
render() {
const selectionList = <div>will be visible in the drawer/sidebar</div>;
const myContent = <div>will be the main content of the page</div>;
return (
<DynamicDrawer
drawerContent={selectionList}
mainContent={myContent}
/>
);
}
I have no problem implementing that <DynamicDrawer>, however the <MenuBar> of the application component needs some connection to the active <DynamicDrawer>:
when using the Drawer, the <MenuBar> must show the left icon, otherwise not
when the user clicks/taps on that icon, the <Drawer> must be toggled
Should I use some store like Redux to solve this problem? Or pass handlers and state manually around? Should I redesign the component hierarchy completely?
React makes DOM manipulations very straightforward and alarmingly fast.
However, most of the changes on your DOM should be determined by data, and not manually manipulated by you (even if you have the power to). Break this rule and hell will let loose as your app begins to get increasingly complex.
So no, you shouldn't pass handlers and state manually around, and yes, you may very likely have to redesign the hierarchy (flow is a bit cut off from the MenuBar, almost like it's meant to be static).
I might not be able to say specifically what your new hierarchy should be. This is purely dependent on how you want data to flow.
Here's why React users love libraries like Flux and Redux. Both preach that data should only flow in one direction, and all state changes should only occur though a single dispatch call.
Since you're new to React, and have gotten a hang of the basics, I think it's time to look at Redux. Once you understand it, it will be clear to you where to place not just the Menu bar, but also any other component you wish to add.
Edit 1
Since you will love to manipulate the Menubar using the state of the top-level component, instead of just css rules, then the <MenuBar /> component should be within the top-level component, and not with the Router (as a matter of fact, seeing the <MenuBar /> with the router is very strange anyways :).
Your top-level component's render() could look like this:
render() {
const selectionList = <div>will be visible in the drawer/sidebar</div>;
const myContent = <div>will be the main content of the page</div>;
return (
<div>
<Menubar />
{this.props.children}
</div>
);
}
While your <DynamicDrawer /> stay the same. This way, you could pass callbacks as props and watch for changes in the <DynamicDrawer /> that could be used to influence the visibility of the sidebar.
I'd have to add though. If it's only 'screen resolution' that affects the sidebar visibility, having css rules in place could be one way to go.
By re-reading the Router documentation I noticed that <Route> can be nested and still each <Route> level can have it's own components.
That solves my problem in a very elegant way:
<Router>
<Route path="/" component={AppOuter}>
<Route path="/page1" component={Page1} />
<Route path="/page2" component={Page2} />
<Route path="/page3" component={Page3} />
</Route>
</Router>
...with my MenuBar being a child of AppOuter:
render() {
return (
<div>
<MenuBar />
{this.props.children}
</div>
);
}
That means that while being on page1 this results into...
<AppOuter>
<MenuBar />
<Page1 />
</AppOuter>
That way I can keep my Page components and the permanent parts of my application (like the MenuBar) at the same level, i.e. AppOuter and PageX can receive the same props and I can pass callbacks to the page components which belong to the top level component (containing the Router).
Still, it's probably better to go with this hierarchy but use Redux (or similar) to manage the state.
...React is awesome ;-)

Categories