React-router: Passing props to children - javascript

I'm trying to learn React by building a very basic "portfolio" site with react-router. My main components are: App, Work, Project and ProjectDetail. On the 'Work' page you should be able to see all of the project thumbnails and titles. Clicking on a specific project should route you to that project's detail page, which will include the rest of the project's details. How can I pass the props of Project to ProjectDetail?
My files:
main.js
/*
Routes
*/
var routes = (
<Router history={createHistory()}>
<Route path="/" component={App}>
<IndexRoute component={Work} />
<Route path="work" component={Work} />
<Route path="work/:id" component={ProjectDetail} />
<Route path="about" component={About} />
</Route>
</Router>
);
ReactDOM.render(routes, document.getElementById('main'));
-
App.js
/*
App
*/
class App extends React.Component {
render() {
return (
<div>
<h1>App</h1>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/work">Work</Link></li>
<li><Link to="/about">About</Link></li>
</ul>
{this.props.children}
<footer>footer</footer>
</div>
)
}
}
export default App;
-
Work.js
class Work extends React.Component {
render() {
return (
<div>
<p>Work</p>
<ul>
{/* Need to loop of all projects */}
{PROJECTS.map(project => (
<li key={project.id}>
<Project project={project} />
</li>
))}
</ul>
</div>
)
}
}
export default Work;
-
Project.js
class Project extends React.Component {
render() {
var project = this.props.project;
var linkTo = "/work/" + project.id;
return (
<Link to={linkTo}>
{project.title}
<span> </span>
{project.type}
</Link>
)
}
}
export default Project;
-
ProjectDetail.js
class ProjectDetail extends React.Component {
render() {
return (
<div>
{/* THIS IS WHAT INFORMATION I NEED */}
{this.props.project.title}
{this.props.project.description}
{this.props.project.type}
{this.props.project.technologies}
</div>
)
}
}
export default ProjectDetail;
I can access the data in my Project component, but how can I pass that along to ProjectDetail since I never explicitly use the ProjectDetail component (only used in my routing)?
EDIT: I should add that I suppose I am technically not trying to pass the props to a child since Project is no longer rendered once you are routed to a ProjectDetail. Does that make this impossible?

You need to use the createElement handler for your routes to control the instancing of the route handler. There you can pass props around to the components. React-Router decouples mother-child components, any state propagation needs to be done in the routes.
Update
Based on your edit, in your case ProjectDetail must re-query the applicable project using this.props.params.id, something like...
ProjectDetail.js
class ProjectDetail extends React.Component {
render() {
let project = PROJECTS.find((item)=>item.id == this.props.params.id);
return (
<div>
{project.title}
{project.description}
{project.type}
{project.technologies}
</div>
)
}
}

Related

import css still stays in page when route changes

I am using two main layouts with react router like below. I have written AppRoute component to use different layouts with switch.
Problem is when i come back to "/" home from "/login" or "/register" route some import css related to "LayoutLoginRegister" still stays in browser. So it breaks the page. Because this css belongs to "LayoutLoginRegister" not "LayoutLanding"
App.js
const AppWrapper = styled.div`
margin: 0 auto;
min-height: 100%;
`;
const AppRoute = ({ component: Component, layout: Layout, ...rest }) => (
<Route
{...rest}
render={(props) => (
<Layout>
<Component {...props} />
</Layout>
)}
/>
);
AppRoute.propTypes = {
component: React.PropTypes.any.isRequired,
layout: React.PropTypes.any.isRequired,
};
export default function App() {
return (
<AppWrapper>
<Helmet
titleTemplate="%s - React.js Boilerplate"
defaultTitle="React.js Boilerplate"
>
<meta name="description" content="A React.js Boilerplate application" />
</Helmet>
<Switch>
<AppRoute exact path="/" layout={LayoutLanding} component={HomePage} />
<AppRoute path="/features" layout={LayoutLanding} component={FeaturePage} />
<AppRoute path="/login" layout={LayoutLoginRegister} component={LoginPage} />
<AppRoute path="/register" layout={LayoutLoginRegister} component={RegisterPage} />
</Switch>
</AppWrapper>
);
}
LayoutLoginRegister Layout
import React from 'react';
import PropTypes from 'prop-types';
import './LayoutLoginRegister.scss';
export class LayoutLoginRegister extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<div>
{this.props.children}
</div>
);
}
}
LayoutLoginRegister.propTypes = {
children: PropTypes.object.isRequired,
};
export default LayoutLoginRegister;
Importing CSS with webpack in this way will not load / unload the css when the component mounts / unmounts.
I would recommend namespacing your css styles, because you are using scss & separate files for your layouts, this should be fairly easy.
Wrap the contents of LayoutLoginRegister.scss with
.component-LayoutLoginRegister {
[layout login styles go here]
}
Then add a class to your LayoutLoginRegister component
render() {
return (
<div className="component-LayoutLoginRegister">
{this.props.children}
</div>
);
}
The CSS will remain loaded, but it will not affect anything other than your LayoutLoginRegister component.
If you need to apply styles to something shared, like <body>
You can add a class to the body / HTML element when a component mounts / unmounts.
export class MyComponent extends React.Component {
componentDidMount() {
document.body.classList.add('MyComponent-mounted');
}
componentDidUnmount() {
document.body.classList.remove('MyComponent-mounted');
}
}
Although I would generally avoid this unless absolutely necessary, as it couples your component with document.body and makes the component unpure, harder to reuse, etc.

Best way to change React state at <App /> level based on sub-components

I have a React app that has a top-level <App /> component (built starting from create-react-app). My top level component looks something like this:
<Header title={this.state.appTitle} theme={this.state.theme} />
<Switch>
{routes.map((route, index) => <Route key={index} {...route} />)}
<Route component={NotFound} />
</Switch>
(routes is an array of { component:, to: } objects). Each component rendered by <Route> uses a sub-component called <Page> where I set a title and wrap some content:
<Page title="About Us">
<p>Content here</p>
</Page>
Sometimes a page might use a different theme which I want to apply to the <Header /> when that page is being viewed:
<Page title="About Us" theme="alt">
What I'm looking to do is change appTitle and theme state in <App /> when each component is rendered. What is the best way to do this? Using one of React's life-cycle hooks? Some other method to change the "top-level" state? How can I pass an action down to these components through the react-router <Route> component if so?
You can pass a function to each component, and call that function when each child gets mounted, with componentDidMount lifecycle method.
<Switch>
{routes.map((route, index) => {
const Comp = route.component;
return <Route
key={index}
{ ...route}
// overwrite the component prop with another component
component={
(routeProps) => (
<Comp
{...routeProps}
childHasMounted={() => this.setState({ name: route.name })}
/>
)
}
/>
})}
<Route component={NotFound} />
</Switch>
// child.js
class Child extends React.Component {
componentDidMount() {
this.props.childHasMounted();
}
}
Flip the structure on its head. Have every "page" component control their own layout.
Make a layout higher order component (function that takes a component class and returns a component class):
function LayoutHOC({ title, theme, component: ContentComponent }) {
return class LayoutWrapper extends React.Component {
render() {
return (
<div>
<Header title={title} theme={theme} />
<Page title={title} theme={them}>
<ContentComponent {...this.props} />
</Page>
</div>
)
}
}
}
Make the folder structure domain specific, as in pages/about/MyAboutPage.jsx to hold the main component content.
Then make pages/about/index.js and export the content component wrapped in the layout higher order component.
index.js:
import MyAboutPage from './MyAboutPage';
export default LayoutHOC({ title: 'my title', theme: 'alt', component: MyAboutPage })
Then in your routes you can import About from './pages/about' (since it uses index.js you don't have to worry about nested folder structure).
The downside is you have to create an index.js for each domain/route. The upside is your content component doesn't know about its layout, and every page/route can controll its own header/footer however you want.
This pattern is stolen from this React boilerplate project

Get path name of route globally in react

I have a basic routing SPA working using react-router-dom v4 and I would like to get the current route (path name) for use in my header (outer context of the app - not sure if that's accurate nomenclature). I want to have a button in my app bar which will do something depending on the route currently in use.
index.js
ReactDOM.render((
<MuiThemeProvider>
<div>
<Router history={history}>
<div>
<Header />
<MainView />
</div>
</Router>
</div>
</MuiThemeProvider>
), document.getElementById('app'));
header.js
class Header extends React.Component {
constructor(props){
super(props);
this.state = {
open: false
};
}
toggleDrawer(){
this.setState({open: !this.state.open}, ()=> {console.log(this.state)});
}
render() {
return (
<div>
<AppBar
iconClassNameRight="muidocs-icon-navigation-expand-more"
onLeftIconButtonTouchTap={()=>{this.toggleDrawer()}}
iconElementRight={<FlatButton label="Create New"/>}
/>
...
In the header.js I want access to the route's pathname to call a certain function from the <FlatButton /> on the right of the appbar. I've tried {this.props.location.pathname} as per the v4 docs but only got errors. TBH I was probably using it wrong though.
That prop is only provided to components rendered as the child of a Route. If you want it somewhere else (like in your Header), you can use the withRouter helper method to inject the props into your component:
// Header.js
import { withRouter } from 'react-router';
// private header class
class Header extends React.Component {
render() {
// you can access this.props.location here
}
}
// wrap Header class in a new class that will render the
// Header class with the current location
// export this class so other classes will render this
// wrapped component
export default withRouter(Header);
// index.js
// ....
ReactDOM.render((
<MuiThemeProvider>
<div>
<Router history={history}>
<div>
<Header />
<MainView />
</div>
</Router>
</div>
</MuiThemeProvider>
), document.getElementById('app'));
You should use react-router-dom if you are not using it (it's the default react-router package for the web now).
import { BrowserRouter, Route, Link } from 'react-router-dom';
ReactDOM.render((
<MuiThemeProvider>
<div>
<BrowserRouter>
<div>
<Route component={Header} />
<Route exact path="/" component={MainView} />
</div>
</BrowserRouter>
</div>
</MuiThemeProvider>
), document.getElementById('app'));
and then from the header.js try using
this.props.location.pathname

React Router component handling as a variable

I am trying to implement React-Router into an existing react app.
How can I use react-router to display components based on some conditions.
var displayRHS;
if(this.state.displayEventComponent){
{/*
* Events Menus
*/}
displayRHS = <DisplayEventComponent
parentFunc={this.displayComponentFunction}
parentPropDay={this.state.day}
/>
} else if (this.state.displayToDoListComponent){
{/*
* To Do List Menu
*/}
displayRHS = <DisplayToDoListComponent
parentCallback_2={this.updateDisplayToDoListComponent}
updateList={this.state.updateDisplayToDoListComponent}
displayIssuesNotList={false}
/>
} else if (this.state.displayIssuesComponent) {
{/*
* Issues menu
*/}
displayRHS = <DisplayIssuesComponent
parentCallback_2={this.updateDisplayToDoListComponent}
updateList={this.state.updateDisplayToDoListComponent}
displayIssuesNotList={true}
/>
}
Displaying Routes breaks
<Route exact path="/" component={displayRHS} />
How can I display these components with their respective props passed in as well?
Many Thanks in advance
PS, I am kind of thinking that a single page should be just that single page and using a routing library should be a sign that you should just have a page refresh instead..
A single page application is called "single page" because the client fetches only one HTML page from the server side. A single page application can have multiple "client-side pages".
The application you are migrating used some condition because it didn't have a router. In react-router, the condition is matching a URL.
The react router allows you to navigate to a client-side page. You will use a component called <Link> to navigate to a client-side page. A virtual page is just a React component. Each available route needs to define a Route. You will need one Route for each client-side page:
<Router>
<Route exact path="/" component={LayoutComponent}>
<IndexRoute component={ToDoPage} />
<Route path="/events" component={EventsPage} />
<Route path="/issues" component={IssuesPage} />
<Route/>
</Router>
The LayoutComponent will always be rendered:
class LayoutComponent extends React.Component {
render() {
return (
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/events">Events</Link></li>
<li><Link to="/issues">Issues</Link></li>
</ul>
{this.props.children}
);
}
}
The value of this.props.children will be the page that matches the URL. So if the URL is /issues the component rendered in {this.props.children} will be the IssuesPage because we configured it that way:
<Route path="/issues" component={IssuesPage} />
Each of your pages can then render the components
ToDoPage:
class ToDoPage extends React.Component {
constructor() {
this.state = {
updateDisplayToDoListComponent: []
};
}
render() {
return (
<DisplayToDoListComponent
parentCallback_2={this.updateDisplayToDoListComponent}
updateList={this.state.updateDisplayToDoListComponent}
displayIssuesNotList={false} />
);
}
public updateDisplayToDoListComponent() {
// do something
}
}
EventsPage:
class EventsPage extends React.Component {
constructor() {
this.state = {
day: 1
};
}
render() {
return (
<DisplayEventComponent
parentFunc={this.displayComponentFunction}
parentPropDay={this.state.day} />
);
}
public displayComponentFunction() {
// do something
}
}
IssuesPage:
class IssuesPage extends React.Component {
constructor() {
this.state = {
updateDisplayToDoListComponent: []
};
}
render() {
return (
<DisplayIssuesComponent
parentCallback_2={this.updateDisplayToDoListComponent}
updateList={this.state.updateDisplayToDoListComponent}
displayIssuesNotList={true} />
);
}
public updateDisplayToDoListComponent() {
// do something
}
}
This is not going to work out of the box and you are going to need to do some reading of the react-router docs but you should have enough details to figure out how to get it to work. You can also lear from "Getting Started with React Router".

How implement a proper layout using reactjs and react-router

How can I implement a proper layout using reactjs and react-router.
Basically what I want to implement is something line the image below:
Note: I don't want implement the header & footer in the index.html.
So far what I have done and working is:
import React from 'react';
import { Router, Route, Link } from 'react-router'
class App extends React.Component {
render(){
return <div>
<h1>App</h1>
<ul>
<li><Link to="/home">Home</Link></li>
<li><Link to="/blog">Blog</Link></li>
<li><Link to="/portfolio">Portfolio</Link></li>
<li><Link to="/social">Social</Link></li>
<li><Link to="/about">about</Link></li>
</ul>
{this.props.children}
</div>
}
}
class Home extends React.Component {
render(){
return <p>home</p>
}
}
class Blog extends React.Component {
render(){
return <p>blog</p>
}
}
class Portfolio extends React.Component {
render(){
return <p>portfolio</p>
}
}
class Social extends React.Component {
render(){
return <p>social</p>
}
}
class About extends React.Component {
render(){
return <p>about</p>
}
}
let routes = <Router>
<Route path="/" component={App}>
<Route path="home" component={Home} />
<Route path="blog" component={Blog} />
<Route path="portfolio" component={Portfolio} />
<Route path="social" component={Social} />
<Route path="about" component={About} />
</Route>
</Router>
React.render(routes, document.body);
The above code it's working properly, but What I need is separate the the App in 3 components - > <Header />, content and <Footer />
something like:
class App extends React.Component {
render(){
return <Header />
{this.props.children}
<Footer />
}
}
class Footer extends React.Component {
render(){
return <div>Footer</div>
}
}
class Header extends React.Component {
render(){
return <div>
<h1>App</h1>
<ul>
<li><Link to="/home">Home</Link></li>
<li><Link to="/blog">Blog</Link></li>
<li><Link to="/portfolio">Portfolio</Link></li>
<li><Link to="/social">Social</Link></li>
<li><Link to="/about">about</Link></li>
</ul>
</div>
}
}
but when I implement it the routing doesn't work and I don't get any error, I think is something related with the {this.props.children}, so.. any idea how can I get it done?
Your render function transpiles to:
function render() {
return React.createElement(Header, null);
{
this.props.children;
}
React.createElement(Footer, null);
}
You're not seeing any errors because this is valid, but has unreachable code. It just returns a Header element.
You need to wrap its contents in another element, e.g.:
class App extends React.Component {
render() {
return <div>
<Header/>
{this.props.children}
<Footer/>
</div>
}
}
Edit: Imagine you wrote this code outside the context of React - what would you expect it to return?
function render() {
return 'Header';
'Content';
'Footer';
}
These are 3 separate statements, and since the first statement is a return, the last 2 are irrelevant, as they'll never be reached.
In order to return multiple objects from a function, you need to put them in a container of some sort, e.g. an Array:
function render() {
return [
'Header',
'Content',
'Footer'
]
}
However, you can't do this in a React component's render() method, as they must return either a React component, null, or false, hence you need to wrap contents with another element if you want to return multiple items.

Categories

Resources