ReactJS, React-Router: Calling parent function - reactjs

I am working on a React App, trying to call a parent method from a child component, some code of the parent component below:
class NavigationBar extends Component {
constructor(props){
super(props);
this.state={isLoggedIn: false};
}
updateLoginState(){
alert("Login from NavigationBar");
}
GetBar() {
//const isLoggedIn = this.props.isLoggedIn;
if (false){ //isLoggedIn
return this.UserNavBar();
}
return this.StrangerNavBar();
}
StrangerNavBar(){
return (
<div>
<HashRouter>
<div>
{/* ... */}
<div className="content">
<Route exact path="/LoginCC" loginUpdate={this.updateLoginState} component={LoginCC} />
</div>
</div>
</HashRouter>
</div>
);
}
render() {
return (
this.GetBar()
);
}
}
export default NavigationBar;
This component is supposed to redirect the user to different content pages based on whether or not he is logged in, using a Router. If a button is clicked in LoginCC.js the method updateLoginState should be invoked which just displays a message for now. The child content page LoginCC.js looks as follows:
class LoginCC extends Component {
constructor(props){
super(props);
this.state = {isLoggedIn: false};
}
render() {
return (
<div>
<HashRouter>
{/* ... */}
<Button variant="primary" size="lg" block onClick={this.props.loginUpdate}>
Log in
</Button>
{/* ... */}
</HashRouter>
</div>
);
}
}
export default LoginCC;
I passed the method reference as a prop to LoginCC when rendering this component using the Router, so a message should pop up if I press the button, but nothing happens.
Am I passing the prop incorrectly or something else I've missed? I'm new to React so any help is appreciated.

Route doesn't pass any custom props to components. You should use other method to pass functions.
One of solutions is:
<Route exact path="/LoginCC" render={
props => <LoginCC {...props} loginUpdate={this.updateLoginState}/>
} />
Note that updateLoginState will not get this when called. You should either bind it or declare it as an arrow function to get the correct this.
Also check the Context documentation.

Related

React Component, with a nested component access this.props

I am stuck with a small challenge, and that is the how to have a sub-component access things like this.props (specifically looking for this.props.history). My code is essentially;
// app.js, setup a router...
return (
<Router>
<Route path="/componenta" component={ComponentA}/>
</Router>
);
// Component A has access to this.props
class ComponentA extends Component {
componentDidMount() {
console.log('The props are ', this.props);
}
render() {
return (
<div>
<h1>Hello from Component A</h1>
<ComponentB/>
</div>
);
}
}
// ComponentB does not have access to this.props,
// and I know it is because it is a sub-component here.
class ComponentB extends Component {
componentDidMount() {
console.log('The props are ', this.props);
}
render() {
return (
<div>
<h1>Hello from Component B</h1>
</div>
);
}
}
I feel like it is something simple I am missing, any suggestions?
Thanks.
React router automatically passes ComponentA props that belong to react router. That's why you see them in ComponentA. It will provide match, location, and history (docs).
You can think of Route as being something like this:
class Route extends React.Component {
render() {
if (props.path == *actual_url*) {
return React.createElement(props.component, {history, match, location});
// Which in your case results in
// <ComponentA history={history} location={location} match={match} />
} else {
return null;
}
}
}
Note: This is a very simplified illustration. Actual Route class is here and is a little more complicated.
So you have to manage those props from there. To access them in children components, you will need to pass the props you want explicitly like this:
<ComponentB
history={this.props.history}
location={this.props.location} // If needed
match={this.props.match} // If needed
/>
Or if you know you want to pass all props to the nested component, you could use this shorthand:
<ComponentB {...this.props} /> // will result in same props as above example
Personally I prefer to be explicit and use the first example, as just doing {...this.props} tends to lead to components receiving way more props than they need. But that much is up to you.
Change your line where you use ComponentB to include history if you want to use it inside ComponentB:
<ComponentB history={this.props.history} />
You're failing to pass props from ComponentA to ComponentB.
return (
<div>
<h1>Hello from Component A</h1>
<ComponentB passedProps={this.props} />
</div>
);
welcome to SO, i see that you are not passing any prop to the ComponentB so you have to do something like this;
<ComponentB history={this.props.history}/>
And then you will be able to use the history on your ComponentB

How to set a state variable in the parent of a React Route?

I am having fun implementing routing in my app, but find it hard to set a page title in the parent with the name of the screen that Route is rendering, like this:
class App...
setTitle(title) {
this.state.screentitle = title
this.setState(this.state)
}
...
<h1>{this.state.title}</h1}
...
<Route path='/Search' render={props => <ScreenSearch setTitle={this.setTitle} {...props} />} />
and each child holds the actual title text, like:
class ScreenSearch...
componentDidMount() {
this.props.setTitle("Search");
}
While this actually works, I would prefer to keep the title texts for all child screens in the parent, together with all Route rules. After all, the child objects should do just their job, like implementing a search page, but have no need to know what it is called at the parent level.
Also, this seems a complex way with too much code to just set a stupid title.
As a beginner with React Route, I would like to ask if there is a better way.
You can implement a Layout component that will have shared items on it like page title,navigations etc.
Layout Component
class DefaultLayout extends React.Component{
render() {
return (
<div>
<div className="header">
<h1>Company Name</h1>
<Navigation />
</dv>
<div className="content">
<h1>{this.props.title}</h1>
{this.props.children}
</div>
<div className="footer">
<p>Copyright (c) 2014 Company Name</p>
</div>
</div>
);
}
};
module.exports = DefaultLayout;
and the child page
var React = require('react');
var DefaultLayout = require('./DefaultLayout');
class PageOne extends React.Component{
this.state={
title:"Page 1"
}
render() {
return (
<DefaultLayout title={this.props.title}>
<p>The page's content...</p>
<p>goes here.</p>
</DefaultLayout>
);
}
}
This is a way to implement it, if you have many routes you can create your own Route component where you give the title as a prop
<Route path='/Search' render={props => {
this.state.screentitle!==this.setTitle && this.setTitle(this.setTitle)
return <ScreenSearch {...props} />}
/>
After seeing your comment I understand you want to remove the set Title so you will need the location from react-router and an object something like this:
let routeTitles = {
/'search': 'The search' ,
'/jobs': 'Look for a job'
}
<Title>{routeTitles[this.props.location.pathname]}</Title>

How can I wait for init config to complete?

I've this code
class ConnectedApp extends Component {
constructor(props) {
super();
props.initConfig(); // the ajax call that populate the user settings
}
render() {
return (
<I18nextProvider i18n={i18n}>
<Router>
<div className="App" style={appStyle}>
<Head/>
<div className="Container">
<Container/>
</div>
<Foot/>
<Loading/>
<ToastContainer position="bottom-right" />
</div>
</Router>
</I18nextProvider>
);
}
}
Now the problem is that the initconfig is an ajax function in middleware. Before rendering the app for logged user I need to wait that the function has finished. Anyone have some suggestion?
Actually the app works nut on first login give an error and that error is resolved by manual refresh.
You'll need to change a few things here– Firstly you will need some way to indicate loading. This can be done in it's simplest form using a boolean either in your global or local state. You should also move your AJAX call method into the appropriate component lifecycle method componentDidMount.
You want it in your componentDidMount to ensure the component is mounted and ready to receive props or state changes.
class ConnectedApp extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.initConfig();
}
render() {
if (this.props.loading === true) {
return null // this will render nothing until loading is `false`
}
return (
<I18nextProvider i18n={i18n}>
<Router>
<div className="App" style={appStyle}>
<Head/>
<div className="Container">
<Container/>
</div>
<Foot/>
<Loading/>
<ToastContainer position="bottom-right" />
</div>
</Router>
</I18nextProvider>
);
}
}
In your redux state you would want to set a property for the loading state. When you start the request, you would set loading to true, when it is successful set it to false. If it fails, you would need a more expandable solution to account for that other than a simple 'loading' boolean.
Also, if you haven't checked out the new React Hooks API, this is what your component would look like using that.
import React, { useEffect } from "react";
const ConnectedApp = ({ initConfig, loading }) => {
useEffect(() => {
initConfig() // this will only get called when the component mounts. Same as `componentDidMount`
}, [])
if (loading === true) {
return null // this will render nothing until loading is `false`
}
return (
<I18nextProvider i18n={i18n}>
<Router>
<div className="App" style={appStyle}>
<Head/>
<div className="Container">
<Container/>
</div>
<Foot/>
<Loading/>
<ToastContainer position="bottom-right" />
</div>
</Router>
</I18nextProvider>
);
}

Adding a Link as a child of a Router with ReactDOM.render yields "You should not use <Link> outside a <Router>"

I am looking for a way to use ReactDOM.render to create a Link within a react router. The setup more or less looks like this:
const router = (
<div>
<Router>
<Route path="/map" component={Map}/>
</Router>
</div>
);
The relevant parts of Map.jsx look like this:
const MapPopup = () => {
return (
<Link to={`/map/add`} />
)
}
class Map extends React.Component {
componentDidMount() {
this.map = L.map('map')
//...stuff...
this.map.on('contextmenu', event => {
popup
.setLatLng(event.latlng)
.addTo(this.map)
.setContent(
ReactDOM.render(
MapPopup(),
document.querySelector('.leaflet-popup-content')
)[0]
)
.openOn(this.map)
})
}
render() {
return (
<React.Fragment>
<div id="map" />
</React.Fragment>
)
}
}
I am basically trying to add a Link to the map popup provided by leaflet (I can't use react-leaflet for this project). If I however return the MapPopup directly in the render function it works (obviously not in the popup but the Link does work this way).
<React.Fragment>
<div id="map" />
<MapPopup />
</React.Fragment>
Does anyone have an idea how I can tackle this rather unusual problem?
I am using "react-router-dom": "4.3.1".
This is the expected error since <Link> component expects ancestor component to be of router type (<BrowserRouter>, <MemoryRouter>, <Router> ... ), refer this thread for a more details.
For your scenario to circumvent this limitation ReactDOM.createPortal could be utilized instead of ReactDOM.render:
<Route
path="/popup"
render={() => (
<Popup>
<div>
Some content goes here
<Link to="/map"> Back to map</Link>
</div>
</Popup>
)}
/>
where
class Popup extends React.Component {
render() {
return ReactDOM.createPortal(
this.props.children,
document.querySelector("#link-render-div")
);
}
}
and
Here is a demo for your reference

Passing data from one page to another while using router and state in ReactJS

I am new to ReactJS. I am trying to figure out how to pass data from one page to another while using router. When I click on Add User button, I want to add the user and navigate back to home page.
I've searched the web for this and found examples where data is transferred from parent to child using props. But here I am using router. How can I find a way to do this?
index.js
ReactDOM.render(
<Router history={browserHistory}>
<Route path="/" component={Home}>
<IndexRoute component={AllUsers}/>
</Route>
<Route path="/add" component={Add}/>
</Router>
, document.getElementById('root'));
Home.js:
class Home extends Component {
render() {
return (
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/add">Add</Link></li>
</ul>
</nav>
<div>
{this.props.children}
</div>
</div>
);
}
}
AllUsers.js
class AllUsers extends Component {
constructor(props) {
super(props);
this.state = {
headingsData: ["Name"],
rowsData: []
}
}
componentDidMount() {
this.setState({
rowsData: [{name: "Alex"}, {name: "Jack"}]
})
}
render() {
return (
<div className="allUsersContainter">
<h2>All users</h2>
<table border="1" className="tableComp">
<Headings headings={this.state.headingsData}/>
<tbody>
<Rows rows={this.state.rowsData}/>
</tbody>
</table>
</div>
);
}
}
class Headings extends Component {
render() {
let headings = this.props.headings.map((heading, i) => {
return (<th>{heading}</th>);
});
return (<thead><tr>{headings}</tr></thead>);
}
}
class Rows extends Component {
render() {
let rows = this.props.rows.map((row, i) => {
return (<tr key={i}><td>{row.name}</td></tr>);
});
return (<tbody>{rows}</tbody>);
}
}
export default AllUsers;
Add.js
class Add extends Component {
render() {
return (
<form>
<label>First Name:</label><input placeholder="Name" ref="name" required/><br/>
<button type="submit">Add user</button>
</form>
);
}
}
export default Add;
This is a common problem to share a component state with another (not a child) component.
A good solution, in this case, is to use a state management library. Check out Redux or MobX as they are very common with React applications.
The main benefits of using those libraries with React application is Easy state sharing. This is the most important for you because you can share your state not only with child components as a props but almost for every component in your application.
For easy understanding, you can imagine state management libraries as a global state for your application, which you can modify almost from every component and subscribe to it changes.

Resources