How to link to current page with different param in react router? - javascript

Let's say I have the following routes set up (just an example, actual routes aren't this messy):
<Router>
<Switch>
<Route path="/app/fun/:userid/profile" component={Profile} exact/>
<Route path="/photos/:userid" component={Photos} exact/>
<Route path="/:userid/contact" component={Contact} exact/>
</Switch>
</Router>
From ANY of the pages above, how can I link to the SAME page, but with a different userid?
eg:
/**
* Shared component that's rendered by Profile, Photos and Contact
*/
class SharedComponent extends React.Component {
render() {
const bobId = 423423432;
return (
<div>
<Link to={/* what to put here? */}>
Redirect to the same page but with Bob's userid
</Link>
</div>
);
}
}
export default withRouter(SharedComponent);
Note: using react router v4

Actually, I found a way to do this. Not sure if it's the best way, but it works:
/**
* Shared component that's rendered by Profile, Photos and Contact
*/
class SharedComponent extends React.Component {
render() {
const { location, match } = this.props;
const bobId = 423423432;
const bobUrl = location.pathname.replace(match.params.userid, bobId);
return (
<div>
<Link to={bobUrl}>
Redirect to the same page but with Bob's userid
</Link>
</div>
);
}
}
export default withRouter(SharedComponent);
Essentially, I'm taking the current url (location.pathname) and replacing the current userid (match.params.userid) with the new id (bobId)

Pass userId with path props to shared component like below,
class Profile extends React.Component {
render() {
const bobId = 423423432; // Pass bobId with path from parent to child
return (
<SharedComponent path={"/stuff1/stuff2/`${bobId}`/profile"} />
);
}
}
class SharedComponent extends React.Component {
render() {
return (
<div>
<Link to={this.props.path}>
Redirect to the same page but with Bob's userid
</Link>
</div>
);
}
}
export default withRouter(SharedComponent);

Related

Switch between components inside of an already routed component in react

I have used react-router to switch between 2 pages from the navbar. One of the pages is Home component. Now I have 3 columns as components in the homepage. Left column, middle column, and right column. By a button in the left column, I want to switch between two components in the mid column of the homepage.
function App() {
return (
<div className="App">
<Router>
<Navbar/>
<Switch>
<Route exact path="/">
<Home/>
</Route>
<Route path="/FAQ">
<FAQ/>
</Route>
</Switch>
</Router>
</div>
);
}
At home components ->
class Home extends Component {
render() {
return (
<div className={style.homeFlex}>
<LeftContainer/>
**<MidContainer/>**
**<AnotherMid/>**
<RightContainer/>
</div>
</div>
)
}
}
export default Home;
From a button inside LeftContainer, I want to switch between MidContainer and AnotherMid.
How can I use the router inside of an already routed component Linked from a sibling component? If there is a better way other than using the router to achieve the same, please share that also.
You don't need a route in this situation. You can use a state to render different components in your Home component.
Suppose your LeftContainer component looks like this:
class LeftContainer extends React.Component {
render() {
return (
<div>
<button onclick={() => this.props.handleSwitchComponent()}>
click here to change component
</button>
</div>
);
}
}
You'll need to pass a function through props, then you can use this component in Home component like this:
class Home extends Component {
state = { IsMidContainerVisible: true }
handleSwitchComponent = () => {
this.setState(({ IsMidContainerVisible }) => ({
IsMidContainerVisible: !IsMidContainerVisible;
}));
};
render() {
return (
<div className={style.homeFlex}>
<LeftContainer handleSwitchComponent={this.handleSwitchComponent} />
{/* Now here we need to specify when we want to show components */}
{this.state.IsMidContainerVisible ? <MidContainer /> : <AnotherMid />}
<RightContainer />
</div>
);
}
}
export default Home;

React pass object by routing?

What I want to do is set routing to my object. For example:
I set routing for:
http://localhost:3000/projects
It displays all my projects list (it works pretty ok)
Then I want to choose project and see details, but it doesn't work properly:
http://localhost:3000/projects/3
How it looks like:
When I click to Details button, it sends me to /projects:id but I get an error, that items in project are undefined.
My code. I store all routing in main App.js file:
class App extends Component {
render() {
return (
<div>
<BrowserRouter>
<div>
<Route exact path="/projects" component={ProjectsList} />
<Route exact path="/projects/:id" component={ProjectDetails} />
</div>
</BrowserRouter>
</div>
);
}
}
export default App;
I have ProjectsList.js component (i will include code of it if needed), in ProjectsList.js i have listgroup with Project.js that looks like this:
class Project extends Component {
render() {
return (
<ButtonToolbar>
<ListGroupItem>{this.props.project.name</ListGroupItem>
<Link to={`/projects/${this.props.project.id}`}>
<Button>Details</Button>
</Link>
</ButtonToolbar>
);
}
}
export default Project;
By Link to my browser pass me to proper URL (projects/2... etc) but i dont know how to pass object of project to ProjectDetails.js component. Code of it below:
class ProjectDetails extends Component {
render() {
return <li>{this.props.project.description}</li>;
}
}
export default ProjectDetails;
Could you tell me, how to pass object of project by Link to into ProjectDetails.js? For now, i get description as undefined (its obviouse because i pass nothing to component with details).
Use the render method in your route and pass the props.
<Route exact path="/projects/:id" render={() => (
<ProjectDetails
project = {project}
/>
)}/>
You need to use mapStateToProps here. and wrap your component in the conncet from react-redux.
It should be like:
import {connect} from 'react-redux';
class ProjectDetails extends Component {
render() {
return <li>{this.props.project.description}</li>;
}
}
function mapStateToProps(state, ownProps){
const projectInstance = DATA //the Data you are getting or fetching from the ID.
return { project : projectInstance }
}
export default connect((mapStateToProps)(ProjectDetails))
This is what it will look like..!!
class Project extends Component {
render() {
return (
<ButtonToolbar>
<ListGroupItem>{this.props.project.name</ListGroupItem>
<Link to={`/projects/${this.props.project.id}`}>
<Button>Details</Button>
</Link>
</ButtonToolbar>
);
}
}
function mapStateToProps(state, ownProps){
return { project : { id: ownProps.params.id } }
}
export default connect((mapStateToProps)(Project))

React Router v4 Nested match params not accessible at root level

Test Case
https://codesandbox.io/s/rr00y9w2wm
Steps to reproduce
Click on Topics
Click on Rendering with React
OR
Go to https://rr00y9w2wm.codesandbox.io/topics/rendering
Expected Behavior
match.params.topicId should be identical from both the parent Topics component should be the same as match.params.topicId when accessed within the Topic component
Actual Behavior
match.params.topicId when accessed within the Topic component is undefined
match.params.topicId when accessed within the Topics component is rendering
I understand from this closed issue that this is not necessarily a bug.
This requirement is super common among users who want to create a run in the mill web application where a component Topics at a parent level needs to access the match.params.paramId where paramId is a URL param that matches a nested (child) component Topic:
const Topic = ({ match }) => (
<div>
<h2>Topic ID param from Topic Components</h2>
<h3>{match.params.topicId}</h3>
</div>
);
const Topics = ({ match }) => (
<div>
<h2>Topics</h2>
<h3>{match.params.topicId || "undefined"}</h3>
<Route path={`${match.url}/:topicId`} component={Topic} />
...
</div>
);
In a generic sense, Topics could be a Drawer or Navigation Menu component and Topic could be any child component, like it is in the application I'm developing. The child component has it's own :topicId param which has it's own (let's say) <Route path="sections/:sectionId" component={Section} /> Route/Component.
Even more painful, the Navigation Menu needn't have a one-to-one relationship with the component tree. Sometimes the items at the root level of the menu (say Topics, Sections etc.) might correspond to a nested structure (Sections is only rendered under a Topic, /topics/:topicId/sections/:sectionId though it has its own normalized list that is available to the user under the title Sections in the Navigation Bar).
Therefore, when Sections is clicked, it should be highlighted, and not both Sections and Topics.
With the sectionId or sections path unavailable to the Navigation Bar component which is at the Root level of the application, it becomes necessary to write hacks like this for such a commonplace use case.
I am not an expert at all at React Router, so if anyone can venture a proper elegant solution to this use case, I would consider this to be a fruitful endeavor. And by elegant, I mean
Uses match and not history.location.pathname
Does not involve hacky approaches like manually parsing the window.location.xxx
Doesn't use this.props.location.pathname
Does not use third party libraries like path-to-regexp
Does not use query params
Other hacks/partial solutions/related questions:
React Router v4 - How to get current route?
React Router v4 global no match to nested route childs
TIA!
React-router doesn't give you the match params of any of the matched children Route , rather it gives you the params based on the current match. So if you have your Routes setup like
<Route path='/topic' component={Topics} />
and in Topics component you have a Route like
<Route path=`${match.url}/:topicId` component={Topic} />
Now if your url is /topic/topic1 which matched the inner Route but for the Topics component, the matched Route is still, /topic and hence has no params in it, which makes sense.
If you want to fetch params of the children Route matched in the topics component, you would need to make use of matchPath utility provided by React-router and test against the child route whose params you want to obtain
import { matchPath } from 'react-router'
render(){
const {users, flags, location } = this.props;
const match = matchPath(location.pathname, {
path: '/topic/:topicId',
exact: true,
strict: false
})
if(match) {
console.log(match.params.topicId);
}
return (
<div>
<Route exact path="/topic/:topicId" component={Topic} />
</div>
)
}
EDIT:
One method to get all the params at any level is to make use of context and update the params as and when they match in the context Provider.
You would need to create a wrapper around Route for it to work correctly, A typical example would look like
RouteWrapper.jsx
import React from "react";
import _ from "lodash";
import { matchPath } from "react-router-dom";
import { ParamContext } from "./ParamsContext";
import { withRouter, Route } from "react-router-dom";
class CustomRoute extends React.Component {
getMatchParams = props => {
const { location, path, exact, strict } = props || this.props;
const match = matchPath(location.pathname, {
path,
exact,
strict
});
if (match) {
console.log(match.params);
return match.params;
}
return {};
};
componentDidMount() {
const { updateParams } = this.props;
updateParams(this.getMatchParams());
}
componentDidUpdate(prevProps) {
const { updateParams, match } = this.props;
const currentParams = this.getMatchParams();
const prevParams = this.getMatchParams(prevProps);
if (!_.isEqual(currentParams, prevParams)) {
updateParams(match.params);
}
}
componentWillUnmount() {
const { updateParams } = this.props;
const matchParams = this.getMatchParams();
Object.keys(matchParams).forEach(k => (matchParams[k] = undefined));
updateParams(matchParams);
}
render() {
return <Route {...this.props} />;
}
}
const RouteWithRouter = withRouter(CustomRoute);
export default props => (
<ParamContext.Consumer>
{({ updateParams }) => {
return <RouteWithRouter updateParams={updateParams} {...props} />;
}}
</ParamContext.Consumer>
);
ParamsProvider.jsx
import React from "react";
import { ParamContext } from "./ParamsContext";
export default class ParamsProvider extends React.Component {
state = {
allParams: {}
};
updateParams = params => {
console.log({ params: JSON.stringify(params) });
this.setState(prevProps => ({
allParams: {
...prevProps.allParams,
...params
}
}));
};
render() {
return (
<ParamContext.Provider
value={{
allParams: this.state.allParams,
updateParams: this.updateParams
}}
>
{this.props.children}
</ParamContext.Provider>
);
}
}
Index.js
ReactDOM.render(
<BrowserRouter>
<ParamsProvider>
<App />
</ParamsProvider>
</BrowserRouter>,
document.getElementById("root")
);
Working DEMO
Try utilizing query parameters ? to allow the parent and child to access the current selected topic. Unfortunately, you will need to use the module qs because react-router-dom doesn't automatically parse queries (react-router v3 does).
Working example: https://codesandbox.io/s/my1ljx40r9
URL is structured like a concatenated string:
topic?topic=props-v-state
Then you would add to the query with &:
/topics/topic?topic=optimization&category=pure-components&subcategory=shouldComponentUpdate
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname (uses this.props.location.search)
✔ Uses qs to parse location.search
✔ Does not involve hacky approaches
Topics.js
import React from "react";
import { Link, Route } from "react-router-dom";
import qs from "qs";
import Topic from "./Topic";
export default ({ match, location }) => {
const { topic } = qs.parse(location.search, {
ignoreQueryPrefix: true
});
return (
<div>
<h2>Topics</h2>
<ul>
<li>
<Link to={`${match.url}/topic?topic=rendering`}>
Rendering with React
</Link>
</li>
<li>
<Link to={`${match.url}/topic?topic=components`}>Components</Link>
</li>
<li>
<Link to={`${match.url}/topic?topic=props-v-state`}>
Props v. State
</Link>
</li>
</ul>
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>{topic && topic}</h3>
<Route
path={`${match.url}/:topicId`}
render={props => <Topic {...props} topic={topic} />}
/>
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</div>
);
};
Another approach would be to create a HOC that stores params to state and children update the parent's state when its params have changed.
URL is structured like a folder tree: /topics/rendering/optimization/pure-components/shouldComponentUpdate
Working example: https://codesandbox.io/s/9joknpm9jy
✔ Uses match for Route URL handling
✔ Doesn't use this.props.location.pathname
✔ Uses lodash for object to object comparison
✔ Does not involve hacky approaches
Topics.js
import map from "lodash/map";
import React, { Fragment, Component } from "react";
import NestedRoutes from "./NestedRoutes";
import Links from "./Links";
import createPath from "./createPath";
export default class Topics extends Component {
state = {
params: "",
paths: []
};
componentDidMount = () => {
const urlPaths = [
this.props.match.url,
":topicId",
":subcategory",
":item",
":lifecycles"
];
this.setState({ paths: createPath(urlPaths) });
};
handleUrlChange = params => this.setState({ params });
showParams = params =>
!params
? null
: map(params, name => <Fragment key={name}>{name} </Fragment>);
render = () => (
<div>
<h2>Topics</h2>
<Links match={this.props.match} />
<h2>
Topic ID param from Topic<strong>s</strong> Components
</h2>
<h3>{this.state.params && this.showParams(this.state.params)}</h3>
<NestedRoutes
handleUrlChange={this.handleUrlChange}
match={this.props.match}
paths={this.state.paths}
showParams={this.showParams}
/>
</div>
);
}
NestedRoutes.js
import map from "lodash/map";
import React, { Fragment } from "react";
import { Route } from "react-router-dom";
import Topic from "./Topic";
export default ({ handleUrlChange, match, paths, showParams }) => (
<Fragment>
{map(paths, path => (
<Route
exact
key={path}
path={path}
render={props => (
<Topic
{...props}
handleUrlChange={handleUrlChange}
showParams={showParams}
/>
)}
/>
))}
<Route
exact
path={match.url}
render={() => <h3>Please select a topic.</h3>}
/>
</Fragment>
);
If you have a known set of child routes then you can use something like this:
Import {BrowserRouter as Router, Route, Switch } from 'react-router-dom'
<Router>
<Route path={`${baseUrl}/home/:expectedTag?/:expectedEvent?`} component={Parent} />
</Router>
const Parent = (props) => {
return (
<div >
<Switch>
<Route path={`${baseUrl}/home/summary`} component={ChildOne} />
<Route
path={`${baseUrl}/home/:activeTag/:activeEvent?/:activeIndex?`}
component={ChildTwo}
/>
</Switch>
<div>
)
}
In the above example Parent will get expectedTag, expectedEvent as the match params and there is no conflict with the child components and Child component will get activeTag, activeEvent, activeIndex as the parameters. Same name for params can also be used, I have tried that as well.
Try to do something like this:
<Switch>
<Route path="/auth/login/:token" render={props => <Login {...this.props} {...props}/>}/>
<Route path="/auth/login" component={Login}/>
First, the route with the parameter and after the link without parameter.
Inside my Login component I put this line of code console.log(props.match.params.token); to test and worked for me.
If you happen to use React.FC, there is a hook useRouteMatch.
For instance, parent component routes:
<div className="office-wrapper">
<div className="some-parent-stuff">
...
</div>
<div className="child-routes-wrapper">
<Switch>
<Route exact path={`/office`} component={List} />
<Route exact path={`/office/:id`} component={Alter} />
</Switch>
</div>
</div>
And in your child component:
...
import { useRouteMatch } from "react-router-dom"
...
export const Alter = (props) => {
const match = useRouteMatch()
const officeId = +match.params.id
//... rest function code
}

Show progress bar BEFORE go to route

I'm developing a universal react application using redux. I use react-router v3.
I want to show a progress bar "BEFORE" going to next route (next route is fetching data from API).
for example imagine I am in "Home Page" and I want go to "Submit Page". when I click on the Submit Link (react-router Link) first show a progress bar in "Home Page" and wait for Submit page data fetching and then go to "Submit Page".
My React Routes:
<Route component={App}>
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} component={SubmitPage}/>
<Route path={HomingRoutes.SearchPage} component={SearchPage}/>
<Route path={`${HomingRoutes.DealsPage}`} component={DealsPage}/>
<Route path={`${HomingRoutes.DealPage}/:id(/:title)`} component={DealPage}/>
<Route path={`${HomingRoutes.Detail}/:id(/:title)`} component={DetailPage}/>
<Route path="*" component={NoMatch}/>
</Route>
in Home Page :
<Link to "/Submit" >Submit</Link>
My Submit page Container code is :
class SubmitContainer extends React.Component {
static readyOnActions(dispatch) {
return Promise.all([
dispatch(SubmitActions.fetchSubmitInitialData()),
]);
}
componentDidMount() {
this.props.fetchSubmitInitialData();
}
}
"fetchSubmitInitialData" is an action creator that fetch data from API.
One solution would be to pass a placeholder component as props to your SubmitPage that will render only when data is fetching.
So you can use something like:
class SubmitContainer extends React.Component {
state = {
loading: true
progress: 0,
}
componentDidMount() {
// fetch some data and update the state
// consider updating the progress more often
this.props.fetchSubmitInitialData()
.then(() => {
this.setState({ loading: false, progress: 100 })
})
}
render() {
const Placeholder = this.props.placeholder
// Show the placeholder when loading
if (this.state.loading) {
return <Placeholder loading progress={this.state.progress} />
}
// Otherwise render your component with the data
return <SubmitPage data={/*..*/}>
}
}
And finally pass you could use the component HomePage as placeholder like this:
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} render={(props) => (
<SubmitContainer {...props} placeholder={HomePage} />
)}/>
Here I use the render props with React router v4. But I'm sure there is an equivalent for the version 3
Now HomePage will render during data fetching and can use the props loading and progress to show a spinner or something
You can add onEnter hook into your router and add onEnter.js inside your SubmitContainer folder and move fetchSubmitInitialData to the onEnter.js then import your store here and dispatch it. the implementation might looks like this:
Your React-Route
import { onEnterSubmitPage } from './your onEnter path/onEnter'
<Route component={App}>
<Route path={HomingRoutes.HomePage} component={HomePage}/>
<Route path={HomingRoutes.SubmitPage} component={SubmitPage} onEnter={onEnterSubmitPage}/>
<Route path={HomingRoutes.SearchPage} component={SearchPage}/>
<Route path={`${HomingRoutes.DealsPage}`} component={DealsPage}/>
<Route path={`${HomingRoutes.DealPage}/:id(/:title)`} component={DealPage}/>
<Route path={`${HomingRoutes.Detail}/:id(/:title)`} component={DetailPage}/>
<Route path="*" component={NoMatch}/>
</Route>
create onEnter.js file in SubmitPage container:
/**
* Import dependencies and action creators
*/
import { store } from '../../index'
import { fetchSubmitInitialData } from './actions'
/**
* Define onEnter function
*/
export function onEnterSubmitPage() {
store.dispatch(fetchSubmitInitialData())
}
then we can integrate state for progress bar into redux too.
actions.js
/** Import all dependencies here **/
import axios from 'axios'
import { FETCH_SUBMIT_INITIAL_DATA, IS_FETCHING_INITIAL_DATA } from './constants'
export function fetchSubmitInitialData() {
/** this dispatch is from middleware **/
return (dispatch) => {
/** this will set progress bar to true **/
dispatch(fetchSubmitInitialData(true))
/** Your fetching action here, this will depend on your configuration **/
axios.get(`url`, {{ headers: `bearer //your token`}})
.then( (response) => {
dispatch(fetchSubmitInitialData(false))
})
}
}
export function isFetchInitialData(status) {
return {
type: IS_FETCHING_INITIAL_DATA,
status
}
}
so there is no need to fetch the data inside SubmitPage container.
One solution would be to pass a placeholder component as props to your SubmitPage that will render only when data is fetching.
So you can use something like:
class SubmitContainer extends React.Component {
render() {
/** this come from your reducer **/
const { isFetching, submitInitialData } = this.props
// Show the placeholder when loading
if (isFetching) {
return <Loader />
}
// Otherwise render your component
return <SubmitPage data={/*..*/}>
}
}
// Map state to props
const mapStatetoProps = ({ app }) => {
isFetching: //,
submitInitialData: //
}
export default connect(mapStatetoProps, null)(SubmitContainer)

How to render a nested component with react-router

This is a noob question. I'm developing a universal react application using react-isomorphic-starterkit boilerplate (https://github.com/RickWong/react-isomorphic-starterkit). I would create a "fixed" sidebar on the left that contains links to the other pages rendered on a child of the main container on the right. Here's my code:
routes.js
module.exports = (
<Router history={browserHistory}>
<Route path="/" component={Main}>
<Route path='/inbox' component={Inbox} />
</Route>
</Router>
);
Main.js
export default class Main extends React.Component {
constructor(props, context){
super(props,context);
console.log('Main props', props);
console.log('Main context', props);
}
/**
* componentWillMount() runs on server and client.
*/
componentWillMount () {
if (__SERVER__) {
console.log("Hello server");
}
if (__CLIENT__) {
console.log("Hello client");
}
}
/**
* Runs on server and client.
*/
render () {
return (
<App/>
)
}
}
App.js
class App extends React.Component{
constructor(props, context) {
super(props, context);
console.log('App props ', props);
}
render(){
return (
<div>
<Sidebar/>
<RightContent />
</div>
)
}
}
RightContent.js
class RightContent extends React.Component{
render(){
return (
<div id="b" style={{backgroundColor:"#EEF0F4", width:"100%"}}>
<NavBar />
{this.props.children}
</div>
)
}
}
Problem is: Inbox component (a simple <div>) is not rendering when I click on the sidebar with <Link to="..."> tag. I don't know if the following thing is correct: as you can see, in App.js class constructor prints props variable...but the output is undefined. However, in Main.js props are printed correctly. react-router version is greater than 2. Could anyone help me?
You're not passing props down to App, so unless you use the location context, it won't have them.
You should do...
render () {
return (
<App {...this.props}/>
)
}
Then, you should have access to this.props.children in App to render the nested route(s). You'll need to specify that in RightContent too...
render(){
return (
<div>
<Sidebar/>
<RightContent>
{this.props.children}
</RightContent>
</div>
)
}
See the tutorial in the docs... https://github.com/reactjs/react-router/blob/master/docs/Introduction.md#with-react-router

Categories