Passing custom props to router component in react-router v4 - javascript

I'm using React Router to create a multi page app. My main component is <App/> and it renders all of the routing to to child components. I'm trying to pass props via the route, and based on some research I did, the most common way for child components to tap into props passed down is via the this.props.route object that they inherit. However, this object is undefined for me. On my render() function in the child component, I console.log(this.props) and am return an object that looks like this
{match: Object, location: Object, history: Object, staticContext: undefined}
Doesn't look like the props I expected at all. Here is my code in detail.
Parent Component (I'm trying to pass the word "hi" down as a prop called "test" in all of my child components):
import { BrowserRouter as Router, HashRouter, Route, Switch } from 'react-router-dom';
import Link from 'react-router';
import React from 'react';
import Home from './Home.jsx';
import Nav from './Nav.jsx';
import Progress from './Progress.jsx';
import Test from './Test.jsx';
export default class App extends React.Component {
constructor() {
super();
this._fetchPuzzle = this._fetchPuzzle.bind(this);
}
render() {
return (
<Router>
<div>
<Nav />
<Switch>
<Route path="/" exact test="hi" component={Home} />
<Route path="/progress" test="hi" component={Progress} />
<Route path="/test" test="hi" component={Test} />
<Route render={() => <p>Page not found!</p>} />
</Switch>
</div>
</Router>
);
}
}
Child:
import React from 'react';
const CodeMirror = require('react-codemirror');
import { Link } from 'react-router-dom';
require('codemirror/mode/javascript/javascript')
require('codemirror/mode/xml/xml');
require('codemirror/mode/markdown/markdown');
export default class Home extends React.Component {
constructor(props) {
super(props);
console.log(props)
}
render() {
const options = {
lineNumbers: true,
theme: 'abcdef'
// mode: this.state.mode
};
console.log(this.props)
return (
<div>
<h1>First page bro</h1>
<CodeMirror value='code lol' onChange={()=>'do something'} options={options} />
</div>);
}
}
I'm pretty new to React so my apologies if I'm missing something obvious.
Thanks!

You can pass props to the component by making use of the render prop to the Route and thus inlining your component definition. According to the DOCS:
This allows for convenient inline rendering and wrapping without the
undesired remounting explained above.Instead of having a new React
element created for you using the component prop, you can pass in a
function to be called when the location matches. The render prop
receives all the same route props as the component render prop
So you can pass the prop to component like
<Route path="/" exact render={(props) => (<Home test="hi" {...props}/>)} />
and then you can access it like
this.props.test
in your Home component
P.S. Also make sure that you are passing {...props} so that the
default router props like location, history, match etc are also getting passed on to the Home component
otherwise the only prop that is getting passed down to it is test.

Related

Is function component that uses React-hook can be render inside from a class component

I use react-hook in my all component. Now when I want to render It's inside React_Router BrowserRouter component It's given me an error.
Error Massage: Invalid hook call. Hooks can only be called inside of the body of a function component
If I understand well your problem, I think you could do nested routes with hoo like it.
Here would be your main router for example:
import React from 'react';
import {Switch, Route, withRouter, Link} from "react-router-dom";
import MyComponent from "./MyComponent";
class Main extends React.Component
{
render() {
return (
<div className='main'>
<Switch>
<Route exact path='/test' component={MyComponent} />
</Switch>
</div>
);
}
}
export default withRouter(Main);
And Here would be your routed component with nested route.
import React from 'react';
import {Switch, Route, withRouter} from "react-router-dom";
class MyComponent extends React.Component
{
render()
{
const {path} = this.props.match;
return (
<div className='test'>
<Switch>
<Route path={`${path}/catalog`}>
<div>Route catalog</div>
</Route>
<Route exact path={path}>
<div>Route dashboard</div>
</Route>
</Switch>
</div>
);
}
}
export default withRouter(MyComponent);
I had the same error in the past days. In my case the problem was that i use render prop of Route component instead of component prop
<Route render={FunCompWithHooks} /> {/* wrong */}
<Route component={FunCompWithHooks} /> {/* correct */}

Why is my PhotoComponent not updating with the correct photos?

Here is App.js
import React, {Component} from 'react';
import {
BrowserRouter,
Route,
Switch,
Redirect
} from 'react-router-dom';
import Search from './Search';
import Nav from './Nav';
import '../index.css';
import axios from 'axios';
import apiKey from './Config';
import NotFound from './NotFound';
import PhotoList from './PhotoList';
class App extends Component {
state= {
pictures: []
}
componentDidMount() {
this.getImages()
}
getImages=(query='cats')=> {
axios.get(`https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=${apiKey}&tags=${query}&per_page=24&page=1&format=json&nojsoncallback=1`)
.then(res=> {
const pictures=res.data.photos.photo
this.setState({pictures});
}).catch((error)=> {
console.log("There was an error parsing your data", error);
})
}
render() {
console.log(this.state.pictures);
return (
<div className="container">
<Search />
<Nav getImages={this.getImages} />
<Switch>
<Route exact path="/" render={()=> <Redirect to={'/cats'} />} />
<Route path='/cats' render={()=> <PhotoList getImages={()=>this.getImages} query='cats' data={this.state.pictures}/>} />
<Route path='/dogs' render={()=> <PhotoList getImages={()=>this.getImages} query='dogs' data={this.state.pictures} />} />
<Route path='/computers' render={()=> <PhotoList getImages={()=>this.getImages} query='computers' data={this.state.pictures} />} />
<Route component={NotFound}/>
</Switch>
</div>
)
}
}
export default App;
Here is PhotoList.js
import React, {Component} from 'react';
import Photo from './Photo';
class PhotoList extends Component {
handleImages=()=> {
this.props.getImages(this.props.query);
}
componentDidMount() {
this.handleImages();
console.log(this.props.data)
}
componentDidUpdate() {
console.log(this.props.data)
}
render() {
return (
<div className="photo-container">
<h2>Results</h2>
<ul>
{this.props.data.map((photo,index)=>
<Photo
farm={photo.farm}
server={photo.server}
id={photo.id}
secret={photo.secret}
key={index}
/>
)}
</ul>
</div>
);
}
}
export default PhotoList;
I've passed the getImages function into PhotoList that fetches data and changes the main App's state. The function takes a query string (cats, dogs, or computers). Then the state data is passed down as props and mapped over in the PhotoList component.
My website still only displays cats, even when I type in a different path (ex /dogs, /computers), yet when I console log the query string, I'm clearly entering different values into it. So why am I still getting cats shown? I know by default the query is equal to cats, but when I call the function in PhotoList it should be set to a different value with the query prop.
What am I doing wrong?
I agree with the first answer that checking for changes and calling the function again might solve your problem, since getImages() only runs when the component mounts. I have some more advice that might help you organize your code better according to common practices and help you avoid this problem altogether, just by not necessitating the confusing passing around of variables between components.
It makes the most sense to have getImages() part of the PhotoList component. This is because you always need to call that function when you render a PhotoList. It's not essential to the App component. Read more about this kind of concept here: https://reactjs.org/docs/thinking-in-react.html
Since you are using react router, what you can do is grab the search keyword from the path, from the PhotoList component. This would look like declaring the query as <Route path='/:query' ... > and then in the rendered component (PhotoList), grabbing the parameter with this.props.match.query. Read more: https://tylermcginnis.com/react-router-url-parameters/
If you do this, you can set default values for props using PropTypes and defaultProps. read more: https://blog.bitsrc.io/understanding-react-default-props-5c50401ed37d
The issue is, componentDidMount from PhotoList component executes only once when component first time mounts with query cats.
For next route change it will not execute componentDidMount, and your will not get new data but the old one.
In your componentDidUpdate you have to check if you are getting new props and again call your handleImages function which calls your getImages function,
componentDidUpdate(prevProps) {
console.log(this.props.data)
if(prevProps.query !== this.props.query){
this.handleImages();
}
}
Another issue is getImages function is not getting called.
getImages={()=>this.getImages}
You should do this,
getImages={this.getImages}

How would I restructure my React App, so that I can pass state down to a component on a different route

I have three components routed to different paths. I want to restructure my App so that I can pass state via props from my SubmitProject Component to my Portfolio Component I still want them to have separate paths ie; /portfolio and /SubmitProject I plan to have two browserwindows open to test that when I submit a form on SubmitProject it will show up on Portfolio then I will be using firebase to persist my state to a database.
Do I need to have my state be at a top level Component like App.js and then have the BrowserRouter inside of that? If so how do I recreate the connections I have made from <SubmitProject/> -> <PortfolioForm/> -> <FormAdd/>
My Desired Goal is that when I submit the form from the FormAdd Component when I am on the /submit Route that it will output via state on my Portfolio Component on the /Portfolio Route.
It has been recommend to use a state manager like context api, or something else, but I want to know if there is a way to restructure my App and be able to pass state from a top level component that each component and route share.
Here is my relevant code
components/Router.js
import React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import Portfolio from './Portfolio';
import SubmitProject from './SubmitProject';
import App from './App';
const Router = () => (
<BrowserRouter>
<Switch>
<Route exact path="/" component={App}/>
<Route exact path="/portfolio" component={Portfolio}/>
<Route exact path="/submit" component={SubmitProject}/>
</Switch>
</BrowserRouter>
);
export default Router;
components/App.js // Should My Router be in here?
import React from 'react';
class App extends React.Component {
render() {
return <div>Test</div>
}
}
export default App;
/components/SubmitProject.js
import React from 'react';
import PortfolioForm from './PortfolioForm';
import Section from './Section';
class SubmitProject extends React.Component {
state = {
sections:{}
};
addSection = section =>{
const sections = {...this.state.sections};
sections[`section${Date.now()}`] = section;
this.setState({
sections: sections
});
}
render() {
return(
<React.Fragment>
<h1>Submit Project</h1>
<h2>Enter Project Data</h2>
<ul className="section">
{Object.keys(this.state.sections).map(key => <Section key={key} details={this.state.sections[key]}/>)}
</ul>
<PortfolioForm addSection={this.addSection} />
</React.Fragment>
)
}
}
export default SubmitProject;
/components/PortfolioForm.js
import React from 'react';
import FormAdd from './FormAdd';
class Portfolio extends React.Component {
render() {
return(
<React.Fragment>
<h1>Submit Form</h1>
<FormAdd addSection={this.props.addSection}/>
</React.Fragment>
)
}
}
export default Portfolio;
/components/FormAdd.js
import React from 'react';
class FormAdd extends React.Component {
nameRef = React.createRef();
createSection = (event) =>{
event.preventDefault();
const section = {
name: this.nameRef.current.value
};
this.props.addSection(section);
};
render() {
return(
<React.Fragment>
<form onSubmit={this.createSection}>
<input type="text" ref={this.nameRef} name="name" placeholder="Name"/>
<button type="submit">+ Add Section</button>
</form>
</React.Fragment>
)
}
}
export default FormAdd;
/components/Portfolio.js
import React from 'react';
class Portfolio extends React.Component {
//CAN I GET STATE FROM SubmitProject.js FILE IN HERE? By Restructuring my App Somehow.
render() {
return(
<React.Fragment>
<h1>Portfolio Page</h1>
<h2>List of projects</h2>
</React.Fragment>
)
}
}
export default Portfolio;
UPDATED CODE
I am now getting an error that says FooContext is not defined
components/App.js
import React from 'react';
import SubmitProject from './SubmitProject';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
const FooContext = React.createContext();
class App extends React.Component {
state = {
sections:{}
};
addSection = section =>{
const sections = {...this.state.sections};
sections[`section${Date.now()}`] = section;
this.setState({
sections: sections
});
}
render() {
return (
<FooContext.Provider value={this.state.sections}>
<Router/>;
</FooContext.Provider>
)
}
}
class Router extends React.PureComponent {
render() {
return
<BrowserRouter>
<Switch>
<Route exact path="/" component={Root} />
</Switch>
</BrowserRouter>
}
}
const Root = props => <FooContext.Consumer>{sections => <SubmitProject/> }</FooContext.Consumer>;
export default App;
UPDATED CODE V#2
App.js
import React from 'react';
import SubmitProject from './SubmitProject';
import Home from './Home';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
const FooContext = React.createContext();
class App extends React.Component {
state = {
sections:{}
};
addSection = section =>{
const sections = {...this.state.sections};
sections[`section${Date.now()}`] = section;
this.setState({
sections: sections
});
}
render() {
return (
<FooContext.Provider value={this.state.sections}>
<Router/>;
</FooContext.Provider>
)
}
}
class Router extends React.PureComponent {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/portfolio" component={Portfolio} />
</Switch>
</BrowserRouter>
)
}
}
const Portfolio = props => <FooContext.Consumer>{foo => <SubmitProject/>}</FooContext.Consumer>;
export default App;
SubmitProject.js
import React from 'react';
import PortfolioForm from './PortfolioForm';
import Section from './Section';
class SubmitProject extends React.Component {
render() {
return(
<React.Fragment>
<h1>Submit Project</h1>
<h2>Enter Project Data</h2>
<ul className="section">
{Object.keys(this.state.sections).map(key => <Section key={key} details={this.state.sections[key]}/>)}
</ul>
<PortfolioForm addSection={this.addSection} />
</React.Fragment>
)
}
}
export default SubmitProject;
It has been recommend to use a state manager like context api, or something else, but I want to know if there is a way to restructure my App and be able to pass state from a top level component that each component and route share.
There are problems with this approach.
Considering that App maintains application state, it's necessary to pass it to <Router> as a prop and then to route components that depend on it:
class App extends React.Component {
state = { foo: true };
render() {
return <Router foo={this.state.foo}/>
}
}
const Router = props => (
const RootWithFoo = props => <Root foo={props.foo}/>;
return <BrowserRouter>
<Switch>
<Route exact path="/" component={RootWithFoo} />
...
</Switch>
</BrowserRouter>
);
This puts a restriction on component structure; in order to avoid deeply passed props, Router component should be removed, and Route should be rendered directly in App:
class App extends React.Component {
state = { foo: true };
render() {
const RootWithFoo = props => <Root foo={this.state.foo}/>;
return <BrowserRouter>
<Switch>
<Route exact path="/" component={RootWithFoo} />
...
</Switch>
</BrowserRouter>
}
}
This is a problem that context API and state management libraries (e.g. Redux) address. They allow to provide global state for nested components where it's used.
Another problem is performance. The whole router will be re-rendered on each state update. Again, context API and state management libraries address this. As context API manual states:
All Consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes. The propagation from Provider to its descendant Consumers is not subject to the shouldComponentUpdate method, so the Consumer is updated even when an ancestor component bails out of the update.
So if context provider value updates, it's unnecessary to re-render the whole tree. Context consumer will be re-rendered regardless of this. Since the whole tree will be re-rendered by default, Provider child(ren) should be a pure component to avoid unnecessary re-renders. This is a reason why separated App and Router components may be preferable:
class App extends React.Component {
state = { foo: true };
render() {
return <FooContext.Provider value={this.state.foo}>
<Router/>;
</FooContext.Provider>
}
}
class Router extends React.PureComponent {
render() {
return <BrowserRouter>
<Switch>
<Route exact path="/" component={Root} />
...
</Switch>
</BrowserRouter>
}
}
const Root = props => <FooContext.Consumer>{foo => ...}</FooContext.Consumer>;
When global state is updated, only App and route components that depend on it (Root, etc.) are re-rendered but not Router.

How to pass the React Router location prop to a component?

I'm trying to figure out how to pass React Router's location prop to a component.
I have a route defined like so:
<Route
path="placeholder"
render={(props) => <Navigation {...props} />}
/>
In my Navigation component I do console.log(this.props); in the render method only to get an empty object. What gives? Isn't the location prop supposed to be automatically supplied to any component inside a Route?
By the way, I'm using react-router-dom version 4.2.2.
You need to wrap your component with withRouter in order to inject match, history and location in your component props.
import { withRouter } from 'react-router-dom';
class Navigation extends React.Component {
render() {
const { match, location, history } = this.props
return (
<div>{location.pathname}</div>
)
}
}
export default withRouter(Navigation)
You need to use the withRouter function from 'react-router-dom' on you main component where you setup Routes wrapped in a Switch and you should have access to location prop on Navigation Component by using this.props.location
App.js
Class App extends Component {
render(){
return (
<Aux>
<Switch>
<Route path="/login" exact component={Login}/>
<Route path="/Navigation" component={Navigation}/>
<Redirect to="/login"/>
</Switch>
</Aux>
);
}
}
export default withRouter(App);

ReactJS propagate data between Components

I am working on an SPA with ReactJS. I have a root component App and then several child components. In the the App component I am trying to store some application level state such as logged in user id, and other data. However I am not seeing my state be propagated down the child components.
App
import { Router, Route, Link, IndexRoute, browserHistory, hashHistory } from 'react-router';
import ParameterContainer from './components/parameter/parameter-container';
import NavMenu from './components/navigation/nav-menu';
import {Alert} from 'react-bootstrap';
import SelectFilter from './components/sample/sample-container';
// Main component and root component
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
userId: null,
roles: null,
parameterTypes: {
'STRING': 'STRING',
'BOOLEAN': 'BOOLEAN',
'INTEGER': 'INTEGER',
'DECIMAL': 'DECIMAL'
}
};
}
render() {
return (
<div>
<NavMenu />
<div className="container">
{this.props.children}
</div>
</div>
)
}
}
// page for 404
class NoMatch extends React.Component {
render() {
return (
<div className="container">
<Alert bsStyle="danger">
<h1>404: Not Found</h1>
<h3>The requested resource does not exist!</h3>
</Alert>
<img src="images/404.png" style={{display: 'block', margin: '0 auto', width: 300, height: '*'}} />
</div>
)
}
}
// render the application
ReactDOM.render((
<Router history={hashHistory}>
<Route path="/" component={App}>
<Route path="parameter" component={ParameterContainer} />
<Route path="sample" component={SelectFilter} />
<Route path="*" component={NoMatch}/>
</Route>
</Router>
), document.getElementById('react'))
Child Component
import React from 'react';
export default class ParameterContainer extends React.Component {
constructor(props) {
super(props);
this.state = { parameters: [] };
this.client = rest.wrap(mime);
this.fetchFromApi = this.fetchFromApi.bind(this);
console.log('Props:' + props);
}
render() {
....
}
The this.props does not contain what I expected. I need to pass data down to children components.
State is not propagated to child components - you have to set props on child components to pass down data. You can use React.cloneElement to add properties to children:
let childrenWithProps = React.cloneElement({this.props.children, {
userid: this.state.userId,
...
});
The best way would be to use Redux to manage application data and store application level state in Redux store. If you don't use Redux you can also consider using react Context. According to docs you can use Context if:
you want to pass data through the component tree without having to
pass the props down manually at every level
To pass the state or props to the child component you can explicit them on your Route node
<Route path="parameter" component={ParameterContainer} parentState={this.state} />
You can, then, access them in the child component as props
constructor(props) {
super(props)
console.log('parentState:' + props.parentState);
}
Here are better and more detailed answers: react-router - pass props to handler component

Categories