Passing an event method between siblings component in ReactJS - reactjs

I am practicing in ReactJS and I have a trouble in passing a method between 2 sibling component. I have created React app which has 3 component: MainPage is the parent, FirstPage and SecondPage are two children. In FirstPage component there is a header with some text and SecondPage component has a button. My main goal is to pass the change-header method I defined in FirstPage, through MainPage component, to SecondPage component, so that when I click on the button that event method is fired.
I follow this tutorial https://medium.com/#ruthmpardee/passing-data-between-react-components-103ad82ebd17 to build my app. I also use react-router-dom in MainPage to display two page: one for FirstPage, another for SecondPage
Here is my FirstPage component:
import React from 'react'
class FirstPage extends React.Component{
constructor(props){
super(props)
this.state = {
msg: 'First page'
}
}
componentDidMount(){
this.props.callBack(this.changeText.bind(this))
}
render(){
return(
<div className = "first">
<h2>{this.state.msg}</h2>
</div>
)
}
changeText(){
{/* event method I defined to change the header text*/}
this.setState({msg: 'Text changed !!'})
this.props.history.push('/first')
}
}
export default FirstPage
and MainPage component:
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import React from 'react'
import FirstPage from '../component/FirstPage'
import SecondPage from '../component/SecondPage'
class MainPage extends React.Component{
constructor(props){
super(props);
this.state = {
func : null
}
}
myCallBack(callFunc){
this.setState({func: callFunc})
}
render(){
return(
<div className = "main">
<Router>
<Switch>
<Route path = "/first" render = {(props) => <FirstPage {...props} callBack = {this.myCallBack.bind(this)} />} />
<Route path = "/second" render = {(props) => <SecondPage {...props} myFunc = {this.state.func}/>} />
</Switch>
</Router>
</div>
)
}
}
export default MainPage
Follow the tutorial, I defined the property func inside MainPage state to store the event method from FirstPage. The myCallBack method is used to change the property of state. And I pass that method to the FirstPage by using callBack = {this.myCallBack.bind(this)}. So in the FirstPage, when the this.props.callBack(this.changeText.bind(this)) called, the event method will be stored into MainPage state
And finally my SecondPage commponent:
import React from 'react'
class SecondPage extends React.Component{
render(){
return(
<div className = "second">
<h2>Second page</h2>
<button onClick = {this.props.myFunc}> Click here to change</button>
</div>
)
}
}
export default SecondPage
App.js :
import React from 'react'
import MainPage from './component/MainPage'
function App() {
return (
<div className = "App">
<MainPage/>
</div>
);
}
export default App;
I simply pass the this.state.func that store my event to SecondPage through props. And I think this should be work: when I click the button, React will redirect to the 'FirstPage' and change the header field. But in fact when I clicked, nothing happen. Can anyone show me which part I did wrong ?

Hi Quang.
In order to do this, that is a possible approach you can follow:
For any shared data between two siblings of a parent component, we basically put that shared data in the closest parent, which in this case MainPage
Further:
The content you want show in FirstPage and change by SecondPage should exist in the state of the parent component MainPage
Pass the content to the FirstPage as a prop, such like content={this.state.content}
The function that changes the content changeText should be inside MainPage because it will be changing the specific state that is sent as a prop to the FirstPage
Function, when invoked by SecondPage should be changing the state of the MainPage, which is passed to the FirstPage as the content of the header.
Solution:
- FirstPage:
// Re-write as a functional component, because we won't be using lifecycle methods, thus, no need for it to be class component.
import React from 'react'
const FirstPage = (props) => (
<div className = "first">
<h2>{props.msg}</h2>
</div>
);
export default FirstPage
- SecondPage:
// Re-write as a functional component, because we won't be using lifecycle methods, thus, no need for it to be class component.
import React from 'react'
const SecondPage = (props) => (
<div className = "second">
<h2>Second page</h2>
<button onClick = {props.changeMsg}> Click here to change</button>
</div>
)
export default SecondPage
- MainPage:
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import React from "react";
import FirstPage from "../component/FirstPage";
import SecondPage from "../component/SecondPage";
class MainPage extends React.Component {
constructor(props) {
super(props);
this.state = {
msg: '', //added msg that is shown in FirstPage
};
}
// Added the function changeMsg to modify msg value in the state by the button onClick event from SecondPage.
changeMsg(){
{/* event method I defined to change the header text*/}
this.setState({ msg: 'Text changed !!' })
this.props.history.push('/first')
}
// Cleared some un-used functions.
// passed msg for FirstPage as a Prop
// passed changeMsg to SecondPage as a Prop
render() {
return (
<div className="main">
<Router>
<Switch>
<Route
path="/first"
render={props => (
<FirstPage {...props} msg={this.state.msg} />
)}
/>
<Route
path="/second"
render={props => (
<SecondPage {...props} changeMsg={this.changeMsg.bind(this)} />
)}
/>
</Switch>
</Router>
</div>
);
}
}
export default MainPage;

Related

Reactjs - how to pass props to Route?

I’m learning React Navigation using React-Router-Dom. I have created a simple app to illustrate the problem:
Inside App.js I have a Route, that points to the url “/” and loads the functional Component DataSource.js.
Inside DataSource.js I have a state with the variable name:”John”. There is also a buttonwith the onclick pointing to a class method that’s supposed to load a stateless component named ShowData.js using Route.
ShowData.js receives props.name.
What I want to do is: when the button in DataSource.js is clicked, the url changes to “/showdata”, the ShowData.js is loaded and displays the props.name received by DataSource.js, and DataSource.js goes away.
App.js
import './App.css';
import {Route} from 'react-router-dom'
import DataSource from './containers/DataSource'
function App() {
return (
<div className="App">
<Route path='/' component={DataSource}/>
</div>
);
}
export default App;
DataSource.js
import React, { Component } from 'react';
import ShowData from '../components/ShowData'
import {Route} from 'react-router-dom'
class DataSource extends Component{
state={
name:' John',
}
showDataHandler = ()=>{
<Route path='/showdata' render={()=><ShowData name={this.state.name}/>}/>
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
}
export default DataSource;
ShowData.js
import React from 'react';
const showData = props =>{
return (
<div>
<p>{props.name}</p>
</div>
)
}
export default showData;
I have tried the following, but, even though the url does change to '/showdata', the DataSource component is the only thing being rendered to the screen:
DataSource.js
showDataHandler = ()=>{
this.props.history.push('/showdata')
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
<Route path='/showdata' render={()=>{<ShowData name={this.state.name}/>}}/>
</div>
)
}
I also tried the following but nothing changes when the button is clicked:
DataSource.js
showDataHandler = ()=>{
<Route path='/showdata' render={()=>{<ShowData name={this.state.name}/>}}/>
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
How can I use a nested Route inside DataSource.js to pass a prop to another component?
Thanks.
EDIT: As user Sadequs Haque so kindly pointed out, it is possible to retrieve the props when you pass that prop through the url, like '/showdata/John', but that's not what I'd like to do: I'd like that the url was just '/showdata/'.
He also points out that it is possible to render either DataSource or ShowData conditionally, but that will not change the url from '/' to '/showdata'.
There were multiple issues to solve and this solution worked as you wanted.
App.js should have all the routes. I used Route params to pass the props to ShowData. So, /showdata/value would pass value as params to ShowData and render ShowData. And then wrapped the Routes with BrowserRouter. And then used exact route to point / to DataSource because otherwise DataSource would still get rendered as /showdata/:name has /
DataSource.js will simply Link the button to the appropriate Route. You would populate DataSourceValue with the appropriate value.
ShowData.js would read and display value from the router prop. I figured out the object structure of the router params from a console.log() of the props object. It ended up being props.match.params
App.js
import { BrowserRouter as Router, Route } from "react-router-dom";
import DataSource from "./DataSource";
import ShowData from "./ShowData";
function App() {
return (
<div className="App">
<Router>
<Route exact path="/" component={DataSource} />
<Route path="/showdata/:name" component={ShowData} />
</Router>
</div>
);
}
export default App;
DataSource.js
import React, { Component } from "react";
import ShowData from "./ShowData";
class DataSource extends Component {
state = {
name: " John",
clicked: false
};
render() {
if (!this.state.clicked)
return (
<button
onClick={() => {
this.setState({ name: "John", clicked: true });
console.log(this.state.clicked);
}}
>
Go!
</button>
);
else {
return <ShowData name={this.state.name} />;
}
}
}
export default DataSource;
ShowData.js
import React from "react";
const ShowData = (props) => {
console.log(props);
return (
<div>
<p>{props.name}</p>
</div>
);
};
export default ShowData;
Here is my scripts on CodeSandbox. https://codesandbox.io/s/zen-hodgkin-yfjs6?fontsize=14&hidenavigation=1&theme=dark
I figured it out. At least, one way of doing it, anyway.
First, I added a route to the ShowData component inside App.js, so that ShowData could get access to the router props. I also included exact to DataSource route, so it wouldn't be displayed when ShowData is rendered.
App.js
import './App.css';
import {Route} from 'react-router-dom'
import DataSource from './containers/DataSource'
import ShowData from './components/ShowData'
function App() {
return (
<div className="App">
<Route exact path='/' component={DataSource}/>
{/* 1. add Route to ShowData */}
<Route path='/showdata' component={ShowData}/>
</div>
);
}
export default App;
Inside DataSource, I modified the showDataHandler method to push the url I wanted, AND added a query param to it.
DataSource.js
import React, { Component } from 'react';
class DataSource extends Component{
state={
name:' John',
}
showDataHandler = ()=>{
this.props.history.push({
pathname:'/showdata',
query:this.state.name
})
}
render(){
return(
<div>
<button onClick={this.showDataHandler}>Go!</button>
</div>
)
}
}
export default DataSource;
And, finally, I modified ShowData to be a Class, so I could use state and have access to ComponentDidMount (I guess is also possible to use hooks here, if you don't want to change it to a Class).
Inside ComponentDidMount, I get the query param and update the state.
ShowData.js
import React, { Component } from 'react';
class ShowData extends Component{
state={
name:null
}
componentDidMount(){
this.setState({name:this.props.location.query})
}
render(){
return (
<div>
<p>{this.state.name}</p>
</div>
)
}
}
export default ShowData;
Now, when I click the button, the url changes to '/showdata' (and only '/showdata') and the prop name is displayed.
Hope this helps someone. Thanks.

React Adding two parents for a child component

State is the smart component which store all the states of child components
import React from 'react';
import TextArea from './Components/TextArea';
class State extends React.Component {
constructor(props) {
super(props);
this.state = {
name: ''
}
this.myChangeHandler = this.myChangeHandler.bind(this)
}
myChangeHandler = (event) => {
event.preventDefault();
this.setState({
[event.target.name]:event.target.value
});
}
render() {
return (
<div>
{/* Change code below this line */}
<TextArea name = {this.state.name}
myChangeHandler = {this.myChangeHandler}/>
{/* Change code above this line */}
</div>
);
}
};
export default State;
Now TextArea is the child component which share the input value to state.
import React from 'react';
import '../style.css';
class TextArea extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<input type="text" onChange= {this.props.myChangeHandler} name="name" value={this.props.name}></input>
<h1>Hello, my name is:{this.props.name} </h1>
</div>
);
}
};
export default TextArea;
There are several child components that send the data to state component.
so the app component is used to order the child component.
I need two parents for a child component. please see the image enter image description here
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom'
import './App.css';
import TextArea from './Components/TextArea'
function App() {
return (
<div className="App">
<BrowserRouter>
<Route path = "/new" component= {TextArea} />
</BrowserRouter>
</div>
);
}
export default App;
You can't have two "direct" parent for a single component but you can have multiple indirect parents.
Example:
App is the parent of State
State is the parent of TextArea
App is also a parent of TextArea but not a direct one.
This means that you can pass props (data and functions) from the top parent to all of his children.
If you need to pass a function from App to TextArea you need to pass it by State.
Here a tutorial on how to do it.
You can also use the Context and here's a tutorial on how to do it.

React-router custom prop not passing to component. ternary operator not working correctly

In React i have my App.js page where i keep my states. I'm importing user1.js component to App.js, and in user1.js component i have a link button that takes me to path /user2.
When i click the button, React will set state property called testValue to true and in user2.js page ternary operator should choose the first value - test works because of that. But for some reason it does not work.
Any help?
APP.JS
import React, { Component } from 'react';
import './App.css';
import User1 from './components/user1';
class App extends Component {
constructor(props){
super(props);
this.state = {
testValue:false
};
}
change = () => {
this.setState({
testValue:true
},() => {
console.log(this.state.testValue)
});
}
render() {
return (
<div className="App">
<User1 change={this.change}/>
</div>
);
}
}
export default App;
USER1.JS
import React from 'react';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';
import User2 from './user2.js';
const User1 = (props) => {
return(
<BrowserRouter>
<div>
<Link to ="/user2">
<button onClick={props.change}>Next page</button>
</Link>
<Switch>
<Route path="/user2" exact component={User2}/>
</Switch>
</div>
</BrowserRouter>
); // end of return
};
export default User1;
USER2.JS
import React from 'react';
const User2 = (props) => {
console.log(props)
return(
<div>
{props.testValue ?
<p>test works</p>
:
<p>test does not work</p>
}
</div>
);
};
export default User2;
This is what i expected - test works
This is what i got - test does not work
You want to pass a custom property through to a component rendered via a route. Recommended way to do that is to use the render method.
<Route path="/user2" exact render={(props) => <User2 {...props} testValue={true} />} />
I think a valid inquiry here would be what are you wanting to pass through as an extra prop? whats the use case here? You may be trying to pass data in a way you shouldn't (context would be nice :D).

Watching state from child component React with Material UI

New to React. Just using create-react-app and Material UI, nothing else.
Coming from an Angular background.
I cannot communicate from a sibling component to open the sidebar.
I'm separating each part into their own files.
I can get the open button in the Header to talk to the parent App, but cannot get the parent App to communicate with the child LeftSidebar.
Header Component
import React, {Component} from 'react';
import AppBar from 'material-ui/AppBar';
import IconButton from 'material-ui/IconButton';
import NavigationMenu from 'material-ui/svg-icons/navigation/menu';
class Header extends Component {
openLeftBar = () => {
// calls parent method
this.props.onOpenLeftBar();
}
render() {
return (
<AppBar iconElementLeft={
<IconButton onClick={this.openLeftBar}>
<NavigationMenu />
</IconButton>
}
/>
);
}
}
export default Header;
App Component -- receives event from Header, but unsure how to pass dynamic 'watcher' down to LeftSidebar Component
import React, { Component } from 'react';
import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import RaisedButton from 'material-ui/RaisedButton';
import Drawer from 'material-ui/Drawer';
import MenuItem from 'material-ui/MenuItem';
// components
import Header from './Header/Header';
import Body from './Body/Body';
import Footer from './Footer/Footer';
import LeftSidebar from './LeftSidebar/LeftSidebar';
class App extends Component {
constructor() {
super() // gives component context of this instead of parent this
this.state = {
leftBarOpen : false
}
}
notifyOpen = () => {
console.log('opened') // works
this.setState({leftBarOpen: true});
/*** need to pass down to child component and $watch somehow... ***/
}
render() {
return (
<MuiThemeProvider muiTheme={getMuiTheme(darkBaseTheme)}>
<div className="App">
<Header onOpenLeftBar={this.notifyOpen} />
<Body />
<LeftSidebar listenForOpen={this.state.leftBarOpen} />
<Footer />
</div>
</MuiThemeProvider>
);
}
}
export default App;
LeftSidebar Component - cannot get it to listen to parent App component - Angular would use $scope.$watch or $onChanges
// LeftSidebar
import React, { Component } from 'react';
import Drawer from 'material-ui/Drawer';
import MenuItem from 'material-ui/MenuItem';
import IconButton from 'material-ui/IconButton';
import NavigationClose from 'material-ui/svg-icons/navigation/close';
class LeftNavBar extends Component {
/** unsure if necessary here **/
constructor(props, state) {
super(props, state)
this.state = {
leftBarOpen : this.props.leftBarOpen
}
}
/** closing functionality works **/
close = () => {
this.setState({leftBarOpen: false});
}
render() {
return (
<Drawer open={this.state.leftBarOpen}>
<IconButton onClick={this.close}>
<NavigationClose />
</IconButton>
<MenuItem>Menu Item</MenuItem>
<MenuItem>Menu Item 2</MenuItem>
</Drawer>
);
}
}
export default LeftSidebar;
Free your mind of concepts like "watchers". In React there is only state and props. When a component's state changes via this.setState(..) it will update all of its children in render.
Your code is suffering from a typical anti-pattern of duplicating state. If both the header and the sibling components want to access or update the same piece of state, then they belong in a common ancestor (App, in your case) and no where else.
(some stuff removed / renamed for brevity)
class App extends Component {
// don't need `constructor` can just apply initial state here
state = { leftBarOpen: false }
// probably want 'toggle', but for demo purposes, have two methods
open = () => {
this.setState({ leftBarOpen: true })
}
close = () => {
this.setState({ leftBarOpen: false })
}
render() {
return (
<div className="App">
<Header onOpenLeftBar={this.open} />
<LeftSidebar
closeLeftBar={this.close}
leftBarOpen={this.state.leftBarOpen}
/>
</div>
)
}
}
Now Header and LeftSidebar do not need to be classes at all, and simply react to props, and call prop functions.
const LeftSideBar = props => (
<Drawer open={props.leftBarOpen}>
<IconButton onClick={props.closeLeftBar}>
<NavigationClose />
</IconButton>
</Drawer>
)
Now anytime the state in App changes, no matter who initiated the change, your LeftSideBar will react appropriately since it only knows the most recent props
Once you set the leftBarOpen prop as internal state of LeftNavBar you can't modify it externally anymore as you only read the prop in the constructor which only run once when the component initialize it self.
You can use the componentWillReceiveProps life cycle method and update the state respectively when a new prop is received.
That being said, i don't think a Drawer should be responsible for being closed or opened, but should be responsible on how it looks or what it does when its closed or opened.
A drawer can't close or open it self, same as a light-Ball can't turn it self on or off but a switch / button can and should.
Here is a small example to illustrate my point:
const LightBall = ({ on }) => {
return (
<div>{`The light is ${on ? 'On' : 'Off'}`}</div>
);
}
const MySwitch = ({ onClick, on }) => {
return (
<button onClick={onClick}>{`Turn the light ${!on ? 'On' : 'Off'}`}</button>
)
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
lightOn: false
};
}
toggleLight = () => this.setState({ lightOn: !this.state.lightOn });
render() {
const { lightOn } = this.state;
return (
<div>
<MySwitch onClick={this.toggleLight} on={lightOn} />
<LightBall on={lightOn} />
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>

Pass history as props to an event handler

I am creating a form which after being filled up and the submit button is clicked should navigate to another component. However, I cant seem to be able to pass history as a prop. I assume I am doing something wrong with the bindings of this but cant figure this out. Thanks.
Here is my App.js
import React from 'react';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import {LandingPage} from './landingPage/LandingPage';
import {ReportsPage} from './reportsPage/ReportsPage';
export class App extends React.Component {
render() {
return (
<BrowserRouter >
<Switch>
<Route path="/" exact component={LandingPage}/>
<Route path="/reports"
render={() => <ReportsPage/>}
/>
</Switch>
</BrowserRouter>
);
}
}
Here is my LandingPage.js
export class LandingPage extends React.Component {
constructor(props){
super(props);
this.state = {
...
this.formAnswersUpdater = this.formAnswersUpdater.bind(this)
}
formAnswersUpdater(e) {
e.preventDefault()
...
history.push("/reports")
}
render() {
return (
<div>
...
<MyForm
onClickOnLastInputsForm={e => this.formAnswersUpdater}
/>
</div>
)
}
And here is where my event is happening. MyForm.js
export class MyForm extends React.Component {
render() {
return(
...
<Route render={({history}) => (
<button className="uk-button uk-button-primary uk-width-1-1#s"
style={{backgroundColor:'#41f44f',
color:'#666', margin: 0}}
id='buttonSliders'
/*if done here it works*/
/*onClick={() => {history.push("/reports")}}*/
/*However if passed to the event handler it does not*/
onClick={() => {this.props.onClickOnLastInputsForm}}
>
ClickMe!
</button>
)}/>
)
My react-router-dom version is: "^4.2.2"
Ok, here is how I handled the issue.
Instead of exporting the LandingPage component, I wrapped it in withRouter function and then exported it.
class LandingPage extends React.Component {
constructor(props){
super(props);
this.state = {
...
this.formAnswersUpdater = this.formAnswersUpdater.bind(this)
}
formAnswersUpdater(e) {
e.preventDefault()
...
//added this.props. here
this.props.history.push("/reports")
}
render() {
return (
<div>
...
<MyForm
onClickOnLastInputsForm={e => this.formAnswersUpdater}
/>
</div>
)
}
// wrapped it with withRouter
export default withRouter(LandingPage)
And then in MyForm component I just called the eventHandler.
export class MyForm extends React.Component {
render() {
return(
...
<button className="uk-button uk-button-primary uk-width-1-1#s"
style={{backgroundColor:'#41f44f',
color:'#666', margin: 0}}
id='buttonSliders'
onClick={this.props.onClickOnLastInputsForm()}
>
ClickMe!
</button>
)
I don't know if this will solve all of your problems, but I see something important missing in your LandingPage render function
onClickOnLastInputsForm={e => this.formAnswersUpdater}
You forgot to pass in your argument: e
onClickOnLastInputsForm={e => this.formAnswersUpdater(e)}
Make sure you add this on MyForm as well. You forgot it there as well. Do you get any other errors after fixing those?
edit: After some inspection of the docs, it looks like the typical use of react-router-dom has a different pattern than what you have. Most common pattern with route handling is to have a root set of routes and use Link from the npm package to navigate. I only see history and its derivatives in the docs for react-router. I'm aware that react-router-dom is a derivative of react-router, but I think if you follow the normal patterns for React, it'll be a lot easier for you in the future when you're debugging.
A nice article on it. It even has a sample app using it: https://medium.com/#pshrmn/a-simple-react-router-v4-tutorial-7f23ff27adf

Resources