Data Driven routes/pages/subpages in React? - reactjs

This is a simplified example...
If I want to pass from a route (the redirect is if the subpage isn't specified):
<Route path="/activities" component={PageElements.SectionPage} onEnter={App.RequireAuth}>
<IndexRoute component={Activities.Redirect}/>
<Route path="/activities/:page" component={Activities.PageSelector} />
</Route>
And then do some magic in PageElements:
export class SectionPage extends React.Component {
constructor(props, context) {
super(props, context);
}
render() {
return <div>
<SiteSubHeader location={this.props.location} />
{this.props.children}
</div>
}
}
and then want to render things inside of a page selector for a section:
export class PageSelector extends React.Component {
constructor(props) {
super(props);
}
render() {
this.pages = {
"overview": <Overview />,
// "overview": Overview, // also doesn't work
"another": <Another />,
"yetanother": <YetAnother />
}
var page = this.props.params.page;
return this.pages.page;
// also a million variants of return <div>{this.pages.page}</div> don't work
// this kind of thing DOES work:
// if (page == "overview") return <Overview />
}
const Overview = () => {
return <div>Overview</div>
}
}
Basically I'm looking for a way to easily generate a number of pages and subpages out of data structures. They are all pretty different and will be different layouts with subcomponents etc. Am I going about this the wrong way? I don't want to have a ton of ifs in the render method.
invariant.js:38Uncaught Invariant Violation: PageSelector.render(): A
valid React element (or null) must be returned. You may have returned
undefined, an array or some other invalid object.
I feel like I'm fighting against React here so I'm probably doing something wrong. Any help would be appreciated.

Related

Sharing React Component props with grandchildren components

Im struggling to figure out how to share props with grandchildren of a component in react/typescript. Why is the constructer props getting overwritten?
class Resource extends Component<IResourceProps, IResourceState> {
state:IResourceState = {
}
_finder: new this.props.finder({api: this.props.api})
render() {
return (
<div>
<Switch>
<Route path={this.props.path} component={() => {return this._finder}} />
</Switch>
</div>
)
}
}
export default Resource;
class UserFinder extends React.Component<IResourceFinderProps, IResourceFinderState> implements IResourceFinder {
_data: any[] = []
load = (response: QueryResponse): void => {
this._data = response.records;
}
constructor(props:IResourceFinderProps) {
super(props);
}
componentDidMount() {
this.props.api.query({ //<-------- this.props.api is undefined! Why?
page: 0,
pageSize: 100
}, (response:QueryResponse) => {
this.load(response);
})
}
render() {
const columns = [
{
title: 'Name',
dataIndex: 'firstName',
key: 'firstName'
}
];
return (
<div>
<Table columns={columns} rowKey="userId" dataSource={this._data} />
</div>
)
}
}
export default UserFinder
App.js
<Route path="/users" finder={UserFinder} api={this.api} />
Essentially, I want to share the api prop with all of the children of the Route without having to explicitly set it on each one individually. Perhaps, another solution could be giving the child a reference to the grandparent somehow. What are some of my options?
First, if you just want to pass props through the Router, use the prop render instead of component. E.g.:
<Route path={this.props.path} render={(props) => <Component {...props} additionalProps={...} />} />
If you really want to share state without passing props, you will need something like a global state. There, you have different options.
The formerly most used was redux (see docs and well known tutorial ).
With the React update and the support of hooks, there are simpler ways to build such a global state system. You could build it from scratch with useContext & useReducer like this
Or you could just use ReactN which gives you a global state out of the box (see here)

Unable to prevent rerender with shouldComponentUpdate() or with PureComponent

In /home/:id I have a <LogButtons/> when its clicked logOn() get called so logsignPopUp variable become a <logForm/> component.
In the same page I have a <IframeYoutubeComponent/>, I want to prevent it to rerender when the <logForm/> pop on the screen so the video isn't reloaded.
home.js :
export default class Home extends Component {
constructor(props) {
super(props);
this.state = { logsign: "" };
this.logOn = this.logOn.bind(this);
this.signOn = this.signOn.bind(this);
}
logOn() {
this.setState({ logsign: "log" });
}
render() {
let logsignPopUp = this.props.logsign === "log" ? <LogForm/> : this.state.logsign;
let homePage =
<div>
{logsignPopUp}
<div>
<LogButtons logOn={this.logOn}/>
</div>
<div>
<IframeYoutubeComponent paramsId={this.props.match.params.paramsId}/>
</div>
</div>;
return (
<div>
<Route exact path="/home/:id" component={() => <div>{homePage}</div> } />
</div>
);
}
}
iframeYoutubeComponent.js :
export class IframYoutubeComponent extends Component {
render() {
//this.props.youtube come from Redux state
let src = this.props.youtube.find(el => el.youtubeId === this.props.paramsId);
return (
<iframe src={"https://www.youtube.com/embed/" + src}></iframe>
);
}
}
I tried to return false in shouldComponentUpdate() but its not even called :
shouldComponentUpdate() {
console.log("test");
return false;
}
I tried to use a PureComponent for <IframeYoutubeComponent/> but the video still reload when the <logForm/> pop.
I tried to add key to my components and also tried to put this.state.logsign in Redux but nothing worked out.
I started react since 2 months so I might miss something obvious but I can't find out what... Any idea ?
That's because you are passing an arrow function in a component prop to the Route. This way everytime you generate a new function.
You should pass a react component in this prop or at least a function that returns JSX but this function should be defined once. For example as a class method.
Something like:
<div>
<Route exact path="/home/:id" component={this.renderHomePage} />
</div>
But then of course you have to refactor your logic regarding this logsign prop.

Route components from json

I want to generate my routes in this way :
Error message : "Objects are not valid as a React child (found: object with keys {contents})".
class App extends Component {
menu(){
return {
menu:[
{id:1, text:"Dashboard", component: <Dashboard/>},
{id:2, text:"Users", component: <Users/>},
]
}
}
render() {
return (
<HashRouter>
<Router data={this.menu()}/>
</HashRouter>
);
}
};
class Router extends Component {
render() {
let menu = this.props.data.menu,
contents = menu.map((elem, i) =>{
return <Route exact
path={"/" +elem.text.toLowerCase()}
component={elem.component}/> //How to call the component here ?
}, this);
console.log(contents);
return(
{contents} //Error
//<Route exact path={"/" +elem.text.toLowerCase()} render={Dashboard}/> // Works
)
}
};
I make a example for you using codesanbox.io https://codesandbox.io/s/6l9r1qwkzk
Seems like that props.menu.data don't receive component property.
const Router = ({ data }) => JSON.stringify(data);
and the output as below:
{"menu":[{"id":1,"text":"Dashboard"},{"id":2,"text":"Users"}]}
Pay attention
You can't instantiate a component in your json data like this:
{ id: 2, text: "Users", component: <Users /> }
The error for that is Converting circular structure to JSON
* Edit 1 - Added my localhost test
Strangely the codesandbox.io does not debug the component as a function, so I've re-built it on my local machine
* Edit 2 - Try to use React adopt
Compose render props components link a pro
Reference:
github.com - pedronauck/react-adopt
I hope I've been useful! :)

How would I make it so that props.match.params.id can be accessed to any of the children Components?

I'm currently trying to wrap my head around how to structure a ReactJS application. Here's the relevant information:
Dev Environment: npm, webpack
Dependencies: React, ReactDOM, React Router, React Router DOM
Code: (Cleaned up a bit as to not reveal more sensitive information.)
/***********
TABLE OF CONTENTS
01.0.0 - Imports
02.0.0 - Data
03.0.0 - Components
03.1.0 -- Base
03.2.0 -- Location
03.3.0 -- Main
03.3.2 --- Map
04.0.0 - Render
***********/
/* 01.0 Imports */
import React from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import {
HashRouter,
Route,
Switch,
Link
} from 'react-router-dom';
console.log("Set-up successful");
/* 02.0 Data */
const locationDataArray = {
locations: [
{
"location": "Scenario A",
"slug": "lipsum",
"googleUrl": "https://www.google.com/maps/place/Central+Park/#40.7828687,-73.9675438,17z/data=!3m1!4b1!4m5!3m4!1s0x89c2589a018531e3:0xb9df1f7387a94119!8m2!3d40.7828647!4d-73.9653551",
"mapUrl": "locations/scenA/main-map.png",
"areaMaps": [
{
"name": "Overall",
"url": "inner/map-overall.html"
}, {
"name": "A",
"url": "inner/map-a.html"
}, {
"name": "B",
"url": "inner/map-b.html"
}
],
"overallPDF": "diagram.pdf",
"mapKey": "mapkey.txt",
"mapImagemap": "imagemap.txt"
} // list truncated for your convenience.
],
all: function() {return this.locations},
get: function(id) {
const isLocation = q => q.slug === id
return this.locations.find(isLocation)
}
}
/* 03.0 Components */
/* 03.1 Base */
class App extends React.Component {
constructor() {
super();
console.log("<App /> constructor locationDataArray: " + locationDataArray);
this.state = {
locationData: locationDataArray,
test: 'testing!'
};
}
render() {
console.log("<App /> locationDataArray: " + locationDataArray);
console.log("<App /> state, testing: " + this.state.test);
return(
<div>
<Switch>
<Route exact path='/' component={LocationGrid} />
<Route path='/locations/:id' component={MainMap}/>
</Switch>
</div>
);
}
}
/* 03.2.0 -- Location */
class LocationGrid extends React.Component {
constructor() {
super();
}
render() {
return(
<div>
<h2>Location Grid</h2>
<div>
<Link to="/locations">Main Map</Link>
</div>
<div>
<ul>
{ // this part works with extra items in locationDataArray.
locationDataArray.all().map(q => (
<li key={q.slug}>
<Link to={`locations/${q.slug}`}>{q.location}</Link>
</li>
))
}
</ul>
</div>
</div>
);
}
}
/* 03.3.0 -- Main */
class MainMap extends React.Component {
constructor(props) {
super();
console.log("Main map is on");
const location = locationDataArray.get(
props.match.params.id
);
if (!location) {
console.log("No such location!");
}
if (location) {
console.log("These tests show that the locationDataArray is accessible from this component.");
console.log("A: props.match.params.id is: " + props.match.params.id);
console.log("B: Location is: " + location.location);
console.log("C: Map URL: " + location.mapUrl);
console.log("State is: " + location);
}
}
render() {
return (
<div>
<MapHolder />
</div>
);
}
}
/* 03.3.2 --- Map */
//var mapUrl = require(location.mapUrl);
class MapHolder extends React.Component {
render() {
console.log("And these tests show that the locationDataArray is NOT accessible from this component.");
console.log("1. Map location test, mapUrl: " + location.mapUrl);
console.log("2. Map location test, slug: " + location.slug);
return <div>
<img id="imagemap" className="map active" src={location.mapUrl} width="1145" height="958" useMap="#samplemap" alt="" />
<map name="samplemap" id="mapofimage">
<area target="" alt="1" title="1" href="inner/01.html" coords="288,356,320,344,347,403,315,420" shape="poly"></area>
{/* Additional imagemap areas. Truncated for your conveinence.*/}
</map>
<p>A map would be here if I could figure out the bug.</p>
</div>
}
}
/* 04.0.0 -- Render */
ReactDOM.render((
<HashRouter>
<App />
</HashRouter>
), document.getElementById('container'));
package.json and webpack.config.js are all in a git repository, if needed.
Overall Goal: I want to dynamically serve information to a main component and children components via a React Router route param and JSON object. Essentially, I want to be able to associate a param with an item in the object, so that the component can just refer to something like arrayItem.itemProperty and it can work dynamically.
I am trying to stay away from Redux as much as possible, since I'm already slightly overwhelmed by React now.
The Current Problem:
The array (on lines 26-55) is in the code, but anything outside of 1 component (<MainMap />, lines 113-138) can't access it.
An example of one of these child Components would be <MapHolder />, which is on lines 142-156. (I get ReferenceError: props is not defined in those cases.)
How would I make it so that props.match.params.id (or any other thing that would make this work) can be accessed to any of the children Components?
(Note: I'm well aware that the code isn't perfect. But this is still in early production stages, so I'd like to just resolve that one issue and hopefully the rest will clear up as a result of that.)
When you're using import/export, global variables aren't shared between modules. This is a good thing, but you can export multiple variables from a module. For example, you can export locationDataArray; in your App.js to access it from other modules without breaking the existing exports and imports for that module. That being said you almost never want to share global data in React like this. As a general rule of thumb you should "lift" data that components depend on to the highest component in the tree that needs in (you can even make an extra parent component to make this composition easier if you'd like).
When child components rely on data from part of a parent component's data, you should generally pass in subsets of data from parent components via props. Add a location prop to MapHolder with the actual map data for the given id, instead of making the component look up the id itself. Then in MapHolder's parent, render it like this:
<MapHolder
location={locationDataArray.locations.find(location => location.slug === ID)}
/>
What you're looking for is React's Context feature. It's pretty low-level, and is an unstable API likely to change at any point. Meaning: use with caution.
In some cases, you want to pass data through the component tree without having to pass the props down manually at every level. You can do this directly in React with the powerful "context" API.
TL;DR:
First, declare the shape of the context object you will be making accessible to children, and implement getChildChildContext to actually return it:
class MyComponent extends React.Component {
...
static childContextTypes = {
foo: PropTypes.string // or whatever you have
};
getChildContext() {
return {foo: "some value"};
}
...
}
Then, when you want to access that context in a child component, declare that you wish to do so by declaring contextTypes on your child component:
class SomeChildComponent extends React.Component {
static childContextTypes = {
foo: PropTypes.string
};
render() {
// use in your component by accessing this.context.foo
}
}
If it feels klunky, that's because it's meant to be:
The vast majority of applications do not need to use context.
If you want your application to be stable, don't use context. It is an experimental API and it is likely to break in future releases of React.
You will probably get much better results from using a state management library such as redux or mobx to manage state that should be accessible from arbitrary components.

Passing data from child route to father route

I have a route structure like this:
<Route path="/master" component={MasterPageLayout}>
<IndexRoute path="/master/products" component={ProductsPage}/>
<Route path="/master/customer/:id" component={CustomerDetailsPage}/>
<Route path="/master/product/:id" component={ProductDetailsPage}/>
<Route path="/master/price-table" component={PriceTablePage} />
</Route>
<Route path="/poc" component={DistribuitorPageLayout}>
<IndexRoute path="/poc/inventory" component={InventoryPage}/>
</Route>
Inside the MasterPageLayout I have my header and my sidemenu (common to all the nested routes above him), the props.children is rendered inside those menus structures, but my header has a specific text for each route. How can I pass the text (and maybe some other data) from the child to the father?
Passing data back up the tree is usually handled with callbacks. As you only need to get the value once I'd recommend using one of the mounting lifecycle methods to call the callback.
As you've tagged react-redux, I'll give examples for both a React and Redux. I don't believe the basic react example is actually suitable for your situation, as you are rendering props.children which makes passing the callback down more difficult, but I'll leave it in the answer in case it's useful to someone else. The redux example should be applicable to your problem.
Basic React
You can pass a callback to the child that sets a value in the components state to use when rendering
class Child extends React.Component {
componentWillMount() {
this.props.setText("for example")
}
render() {
return (
<div>whatever</div>
)
}
}
class Parent extends React.Component {
render() {
return (
<div>
<Child setText={(text) => this.setState({text})} />
{this.state.text}
</div>
)
}
}
React/Redux
You could dispatch an action to set the text when the child is mounted that sets a value in the store to render in the parent, e.g.
class ChildView extends React.Component {
componentWillMount() {
this.props.setText("for example")
}
render() {
return (
<div>whatever</div>
)
}
}
const mapDispatchToProps = (dispatch) => {
return {
setText: (text) => dispatch(setParentText(text))
}
}
const Child = connect(null, mapDispatchToProps)(ChildView)
const ParentView = ({ text }) => {
return (
<div>
<Child />
{text}
</div>
)
}
const mapStateToProps = (state) => {
return {
text: state.parent.text
}
}
const Parent = connect(mapStateToProps)(ParentView)
I wont worry showing the action creator and the reducer/store setup. If you're using redux you should be able to figure that bit out.
This approach will also work if Parent doesn't directly render Child, whether it is through props.children or extra layers are introduced. In fact, Parent doesn't event need to be an ancestor of Child at all for this approach to work, as long as both are rendered on the same page.

Resources