Route components from json - reactjs

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! :)

Related

Why id returns as undefined in the console when using this.props.match.params?.id?

I tried to display data relevant to a specific post when click on it. But this.props.match.params?.id not return the id. I used the URL by manually typing the id like '/expense/61362fcbe355c7474eaa45c7'. Then it worked fine. Can anyone help me to solve this problem?
constructor(props) {
super(props);
this.state = {
expense: {},
};
}
componentDidMount() {
const id = this.props.match?.params.id;
axios.get(`/expense/${id}`).then((res) => {
if (res.data.success) {
this.setState({
expense: res.data.expense,
init: 1,
});
console.log(this.state.expense);
}
});
}
Dynamic Route
import ExpenseDetails from './ExpenseDetails';
<Route path="/expense/:id">
<ExpenseDetails />
</Route>
You need to make a link dynamic or push route using the history props.
const id = 'any';
history.push(`/expense/${id}`)
After that, you can access it in your component this.props.match?.params?.id.
You can use const { id } = useParams(); hooks for getting id in your
functonal component.
<Route path="/:id/details">
<Details />
</Route>
<Link to="/2/details">Details</Link>
function Details() {
const { id } = useParams();
return (
<div>
<h2>Now showing post {id}</h2>
</div>
);
}

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)

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.

Get the current route name into the component

I am trying not to use : if(window.location) to get access to my current route.
I am working on a code where the router is already defined, and the component is already written. I am trying to access the router information from the component because I need to apply a classname depending on the route.
Here's how the router looks like:
export const createRoutes = (dispatch, getState) => (
<Route component={AppLayout} >
<Route path="/" component={Home} onEnter={(nextState, replaceState) => {
console.log('on Enter / = ', nextState);
nextState.test = 'aaaa'; //can't seem to pass states here....
}} />
then in my component:
render() {
//I want to access my super router or location . in fact
// I just want to know if currentURl === '/'
//and after reading the react-router doc 5 times - my head hurts
// but I still hope to get an answer here before putting a window.location
}
`
Depending on which version of React Router you're using you have access to different objects available on the context of a component.
MyComponent.contextTypes = {
location: React.PropTypes.object
}
which will enable you to do the following
render() {
if (this.context.location.pathname === '/') {
return <h1>Index</h1>;
} else {
return <h2>Not index</h2>;
}
}

Resources