This is more of an architectural question regarding react than a specific issue, but what is considered best practice for managing state/props with a layout component and a several child components which are rendered based on the url?
Note: I'm aware that similar questions have been asked, but this is a little bit different. [How to update ReactJS component based on URL / path with React-Router
Lets say I have something like the following code: A profile page (main layout view) with navigation links for profile sub-sections (settings, preferences, account details, etc), and a main panel where each of the sub-section is rendered.
So currently I would have something like this:
my router
routes.js
<Router history={browserHistory}>
<Route path='/profile' component={Profile} >
<IndexRoute component={Summary} />
<Route path='/profile/settings' component={Settings} />
<Route path='/profile/account' component={Account} />
<Route path='/profile/preferences' component={Preferences} />
</Route>
</Router>
and a stripped down version of my profile layout component
profile.js
class Profile extends React.Component {
constructor(props) {
super(props)
}
render(){
let pathName = this.props.location.pathname;
return(
<div className='container profile-page'>
<div className='side-nav'>
<ul>
<li><Link to='/profile'>Summary</Link></li>
<li><Link to='/profile/settings'>Settings</Link></li>
<li><Link to='/profile/account'>My Account</Link></li>
<li><Link to='/profile/preferences'>Preferences</Link></li>
</ul>
</div>
<div className='main-content'>
{this.props.children}
</div>
</div>
)
}
}
export default Profile;
So this kind of works. The child components will render based on the url. But then how do I manage state and props? The way I understand React and Flux, I want the Profile component to manage state and listen to changes on my stores, and to propagate those changes to its children. Is this correct?
My problem is that there doesn't seem to be an intuitive way to pass props to components rendered by this.props.children, which makes me feel like my current architecture and/or understanding of flux is not correct.
A bit of guidance would be much appreciated.
I feel what you are doing is perfectly fine. You are on the right path.
There's a combination of APIs that React provides you with that will take care of exactly what you're not certain about of how to achieve ( way to pass props to components rendered by this.props.children )
First, you need to take a look at cloneElement
It will basically take a React element, clone it, and return another with props that you can change, alter or replace entirely based on your needs.
Furthermore, combine it with the Children Utilities - loop through the children that were provided to your top level component and make the necessary changes to each element individually.
A proposed sample usage could be as simple as
<div className='main-content'>
{React.children.map(this.props.children, (child, index) => {
//Get props of child
const childProps = child.props;
//do whatever else you need, create some new props, change existing ones
//store them in variables
return React.cloneElement(child, {
...childProps, //these are the old props if you don't want them changed
...someNewProps,
someOldPropOverwritten, //overwrite some old the old props
});
)}
</div>
Use these tools to create truly generic and re-usable components, anywhere. More commonly used utilities from Children are map, forEach and toArray. Each with their own respective goals.
Hope this helps.
Related
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.
I've been tasked with building an application in ReactJS that will have roughly 70 - 100 pages / subpages. So far I've tried building this so each page is it's own component i.e. mainPage.js , subPage1.js, subPage2.js etc. but in the long run there will be far too many pages to maintain. Many of the pages follow the same structure like the code below, but each page will of course have it's own unique data.
<Header>
<Main Content>
<Footer>
So my question is, is there a way to dynamically create pages in React or a way of creating component pages so that I don't have to create a seperate page for every single different page?
Use react component composition. Sounds like you've already some header, content, footer components. You should categorize the types of pages your app uses and create general purpose container components for each use-case.
For example, a "base" or "first-level" page component could look like:
const Page = ({ pageHeaderProps, pageFooterProps, ...props }) => (
<Page.Container>
<Page.Header {...pageHeaderProps} />
<Page.Content {...props} />
<Page.Footer {...pageFooterProps} />
</Page.Container>
);
Here the children prop is spread into the (or wrapped by) Content component.
If for example some sub-pages have an additional sub-header or some other common component, then you can compose a component as:
const SubPage = ({ children, subHeaderProps, ...props }) => (
<Page {...props}>
<SubHeader {...subHeaderProps} />
{...children}
</Page>
);
Here the new SubHeader is injected as a child of the Page and will be rendered as part of all the children it renders.
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
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.
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 ;-)