React-Redux: Dispatching on Constructor -> props.store.state not updated - javascript

I am very new at React and Redux both, I have only been using both for about 2 - 3 weeks to develop an alpha version of an application.
Though most of the tutorials on using Redux with React seemed to be pretty complicated I found one that allowed me to get some quick code down to try very simple scenarios within my application.
The main problem I seem to face at the moment is: I want to click on an image of and show the details of said property on another page (routed to using react-router, passing id in the path - to clarify in the current code i am using the hardcoded id of 22 an the id is not passed in the path yet). I thought it would as straight forward as clicking on the app then in either the constructor or componentWillMount method I could call this.props.foo(id) and then get the property using this.props.store.foo but it seems as though the store is not updated at that time. But if i called this.props.foo(id) in the handleClick method of the page before redirecting then it would work, but on refresh the store is back to default and causes an error.
I am just wondering if I am just taking a completely wrong approach to this.. or just missing something.
The code may be too much, just let me know if I should trim it down...
Functions to look for are the:
handleImageClick() - > Results.js
constructor() - > BuyDetails.js
Code:
Index.js
let state = {
results: [],
selectedState:{},
};
let reducer = (state, action) => {
console.log("in reducer" + action.type);
switch (action.type) {
case 'ADD_RESULTS':
console.log("in reducer add");
console.log("in reducer results = " + action.results);
var newState = Object.assign({}, state)
newState.results = action.results
console.log("in reducer add " + JSON.stringify(newState))
return newState
case 'GET_RESULTS':
console.log("in reducer get state = " + state.results[0].id);
var newState = Object.assign({}, state)
for (var result of state.results){
if (result.id === action.id){
console.log(result.img)
newState.selectedState = result
console.log(newState.selectedState.location.address)
}
}
console.log(newState.selectedState.location.address)
console.log(JSON.stringify(newState));
return newState
default:
return state
}
}
let store = createStore(reducer, state)
let mapStateToProps = state => ({
store: state
})
let mapDispatchToProps = dispatch => ({
addResults: (results) => dispatch({type: 'ADD_RESULTS', results:results}),
getSelectedResult: (id) => dispatch({type: 'GET_RESULTS', id:id}),
})
const ConnectedAppComponent = connect(
mapStateToProps, mapDispatchToProps
)(App)
const ConnectedResultsComponent = connect(
mapStateToProps, mapDispatchToProps
)(Results)
const ConnectedBuyDetailsComponent = connect(
mapStateToProps, mapDispatchToProps
)(BuyDetails)
ReactDOM.render(
<Provider store={store}>
<Router history={hashHistory}>
<Route path="/" component={ConnectedAppComponent}/>
{/* add the routes here */}
<Route path="/results" component={ConnectedResultsComponent}/>
<Route path="/buyDetails" component={ConnectedBuyDetailsComponent}/>
</Router>
</Provider>,
document.getElementById('root')
);
Results.js
class Results extends Component{
constructor(props) {
super(props);
this.state = {open: true, openProfile:false, anchorEl: null,dataSet:this.props.store.results};
console.log(this.state.dataSet.length)
console.log(this.state.dataSet[0].img)
}
handleTouchTap = (event) => {
// This prevents ghost click.
console.log("touch tap");
event.preventDefault();
const tempState = this.state;
tempState.openProfile = true
tempState.anchorEl = event.currentTarget
this.setState(tempState)
/*this.setState({
openProfile: true,
anchorEl: event.currentTarget,
});*/
};
handleRequestClose = () => {
const tempState = this.state;
tempState.openProfile = false
tempState.anchorEl = null
this.setState(tempState)
/*this.setState({
openProfile: false,
});*/
};
handleToggle = () => this.setState({open: !this.state.open});
handleImageClick(){
//This is where i could be doing this.props.getSelectedResult(22); and it would work but causes issues on refresh
const path = `/buyDetails`
this.context.router.push(path)
}
render() {
return <MuiThemeProvider>
<div className="Results" id="Results" style={styles}>
<div>
<Toolbar style={appBarStyle}>
<IconButton iconClassName="material-icons"
style={{bottom: '0',height:'auto'}}
onClick={this.handleToggle}>
menu
{/*<FontIcon className="material-icons" color={grey900} onClick={this.handleToggle}>menu</FontIcon>*/}
</IconButton>
<ToolbarGroup style={groupStyle}>
<ToolbarSeparator style={seperatorMargin}/>
<FontIcon style={searchIconnStyle} className="material-icons">search</FontIcon>
<ToolBarSearchField />
</ToolbarGroup>
<ToolbarGroup>
<ToolbarSeparator style={residentialSeperatorStyle}/>
<FlatButton label="Residential" style={selectedToolBarButtonStyle}/>
<ToolbarSeparator style={seperatorStyle}/>
<FlatButton label="Commerical" style={toolBarButtonStyle}/>
<ToolbarSeparator style={seperatorStyle}/>
<FlatButton label="JoellyR" style={toolBarButtonStyle} onTouchTap={this.handleTouchTap}/>
<Popover open={this.state.openProfile}
anchorEl={this.state.anchorEl}
anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
targetOrigin={{horizontal: 'right', vertical: 'top'}}
onRequestClose={this.handleRequestClose}>
<MenuItem value={1} primaryText="Price Range" />
<MenuItem value={2} primaryText="values" />
</Popover>
</ToolbarGroup>
</Toolbar>
<ToolBarFilterFields fieldNames={['Buy', 'Sell', 'Rent', 'Businesses', 'Mortgages']} displaySeperator={false}/>
</div>
<Drawer
open={this.state.open}
containerStyle={{top:'inherit', boxShadow:'(0,0,0,0)', border:'0px', borderRight:'1px solid', borderColor: 'rgba(0,0,0,0.3)'}}>
</Drawer>
<div style={this.state.open ? drawerExpanded : drawerCollapsed }>
<Paper style={paperStyle}>
<ToolBarFilterFields fieldNames={['Filters', 'Price', 'Bath', 'Beds', 'Type', 'Style']} displaySeperator={true}/>
<ResultGridList dataSet={this.state.dataSet} onClick = {() => this.handleImageClick()}/>
</Paper>
</div>
</div>
</MuiThemeProvider>
}
}
Results.contextTypes = {
router: React.PropTypes.object
}
export default Results;
BuyDetails.js
class BuyDetails extends Component{
constructor(props) {
super(props);
//dispatching the action here
this.props.getSelectedResult(22);
//getting the selected object from the props.state ... but it will still be = {}
this.state = {open: true, openProfile:false, anchorEl: null,data:this.props.store.selectedState};
}
componentWillMount() {
}
handleTouchTap = (event) => {
console.log('in buy detail: ' + JSON.stringify(this.props.store.selectedState) + JSON.stringify(this.props.store.results));
// This prevents ghost click.
console.log("touch tap2");
event.preventDefault();
const tempState = this.state;
tempState.openProfile = true
tempState.anchorEl = event.currentTarget
this.setState(tempState)
/*this.setState({
openProfile: true,
anchorEl: event.currentTarget,
});*/
};
handleRequestClose = () => {
const tempState = this.state;
tempState.openProfile = false
tempState.anchorEl = null
this.setState(tempState)
/*this.setState({
openProfile: false,
});*/
};
handleToggle = () => this.setState({open: !this.state.open});
render() {
return <MuiThemeProvider>
<div className="BuyDetails" id="BuyDetails" style={styles}>
<div>
<Toolbar style={appBarStyle}>
<IconButton iconClassName="material-icons"
style={{bottom: '0',height:'auto'}}
onClick={this.handleToggle}>
menu
{/*<FontIcon className="material-icons" color={grey900} onClick={this.handleToggle}>menu</FontIcon>*/}
</IconButton>
<ToolbarGroup style={groupStyle}>
<ToolbarSeparator style={seperatorMargin}/>
<FontIcon style={searchIconnStyle} className="material-icons">search</FontIcon>
<ToolBarSearchField />
</ToolbarGroup>
<ToolbarGroup>
<ToolbarSeparator style={residentialSeperatorStyle}/>
<FlatButton label="Residential" style={selectedToolBarButtonStyle}/>
<ToolbarSeparator style={seperatorStyle}/>
<FlatButton label="Commerical" style={toolBarButtonStyle}/>
<ToolbarSeparator style={seperatorStyle}/>
<FlatButton label="JoellyR" style={toolBarButtonStyle} onTouchTap={this.handleTouchTap}/>
<Popover open={this.state.openProfile}
anchorEl={this.state.anchorEl}
anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
targetOrigin={{horizontal: 'right', vertical: 'top'}}
onRequestClose={this.handleRequestClose}>
<MenuItem value={1} primaryText="Price Range" />
<MenuItem value={2} primaryText="values" />
</Popover>
</ToolbarGroup>
</Toolbar>
</div>
<Drawer
open={this.state.open}
containerStyle={{top:'inherit', boxShadow:'(0,0,0,0)', border:'0px', borderRight:'1px solid', borderColor: 'rgba(0,0,0,0.3)'}}>
</Drawer>
<div style={this.state.open ? drawerExpanded : drawerCollapsed }>
<Paper style={paperStyle}>
<BuyDetailGridList data={this.props.store.selectedState}/>
</Paper>
</div>
</div>
</MuiThemeProvider>
}
}
function isEmpty(obj) {
for(var key in obj) {
if(obj.hasOwnProperty(key))
return false;
}
return true;
}
export default BuyDetails;
Thanks Everyone... In Advance :)
+++UPDATE - Still not working+++
Here is the code for another approach i tried which was just calling the dispatch in componentWillMount() then just passing this.props.store.selectedState directly to the child component.
BuyDetails.js
class BuyDetails extends Component{
constructor(props) {
super(props);
this.state = {open: true, openProfile:false, anchorEl: null,data:{}};
//console.log('in buy details '+ JSON.stringify(this.state.data));
}
componentWillMount() {
//dispatching the action here... it is still this.props.store.selectedState is still = {}
this.props.getSelectedResult(22);
}
handleTouchTap = (event) => {
console.log('in buy detail: ' + JSON.stringify(this.props.store.selectedState) + JSON.stringify(this.props.store.results));
// This prevents ghost click.
console.log("touch tap2");
event.preventDefault();
const tempState = this.state;
tempState.openProfile = true
tempState.anchorEl = event.currentTarget
this.setState(tempState)
/*this.setState({
openProfile: true,
anchorEl: event.currentTarget,
});*/
};
handleRequestClose = () => {
const tempState = this.state;
tempState.openProfile = false
tempState.anchorEl = null
this.setState(tempState)
/*this.setState({
openProfile: false,
});*/
};
handleToggle = () => this.setState({open: !this.state.open});
render() {
return <MuiThemeProvider>
<div className="BuyDetails" id="BuyDetails" style={styles}>
<div>
<Toolbar style={appBarStyle}>
<IconButton iconClassName="material-icons"
style={{bottom: '0',height:'auto'}}
onClick={this.handleToggle}>
menu
{/*<FontIcon className="material-icons" color={grey900} onClick={this.handleToggle}>menu</FontIcon>*/}
</IconButton>
<ToolbarGroup style={groupStyle}>
<ToolbarSeparator style={seperatorMargin}/>
<FontIcon style={searchIconnStyle} className="material-icons">search</FontIcon>
<ToolBarSearchField />
</ToolbarGroup>
<ToolbarGroup>
<ToolbarSeparator style={residentialSeperatorStyle}/>
<FlatButton label="Residential" style={selectedToolBarButtonStyle}/>
<ToolbarSeparator style={seperatorStyle}/>
<FlatButton label="Commerical" style={toolBarButtonStyle}/>
<ToolbarSeparator style={seperatorStyle}/>
<FlatButton label="JoellyR" style={toolBarButtonStyle} onTouchTap={this.handleTouchTap}/>
<Popover open={this.state.openProfile}
anchorEl={this.state.anchorEl}
anchorOrigin={{horizontal: 'right', vertical: 'bottom'}}
targetOrigin={{horizontal: 'right', vertical: 'top'}}
onRequestClose={this.handleRequestClose}>
<MenuItem value={1} primaryText="Price Range" />
<MenuItem value={2} primaryText="values" />
</Popover>
</ToolbarGroup>
</Toolbar>
</div>
<Drawer
open={this.state.open}
containerStyle={{top:'inherit', boxShadow:'(0,0,0,0)', border:'0px', borderRight:'1px solid', borderColor: 'rgba(0,0,0,0.3)'}}>
</Drawer>
<div style={this.state.open ? drawerExpanded : drawerCollapsed }>
<Paper style={paperStyle}>
<BuyDetailGridList data={this.props.store.selectedState}/>
</Paper>
</div>
</div>
</MuiThemeProvider>
}
}
function isEmpty(obj) {
for(var key in obj) {
if(obj.hasOwnProperty(key))
return false;
}
return true;
}
export default BuyDetails;

I wouldn't fetch the item in the details component, at least not explicitly.
Consider:
A details component:
class DetailsComponent extends React.Component {
// the item is now available in props.item
}
function mapStateToProps(state, props) {
return {
item: state.getSelectedItem()
};
}
export default connect(mapStateToProps)(DetailsComponent);
A list component:
class ListComponent extends React.Component {
...
onImageClick = (item) => {
this.props.setSelectedItem(item);
}
...
}
This relies on set/getSelectedItem actions which set some relevant state. The details component will automatically grab the selected item when it's mounted.
Another thing to consider would be, if the two components were being rendered simultaneously (in a list/detail style UI, for example), would be to lift the selected state up into the parent state (parent of both components).
class ParentComponent extends React.Component {
...
onItemSelected = (item) => {
this.setState({ selectedItem: item });
}
render() {
return (
<ListComponent onItemSelected={ this.onItemSelected }/>
<DetailsComponent item={ this.state.selectedItem }/>
);
}
}
That all said, you posted a lot of code and it's a little hard to tell what's going on. Hopefully something I've written above helps with your issue.

Related

React Error boundary unexpected behavior with material ui popover

There is a notifications component within a popover that makes a fetch call.
The connection fails and cannot reach the resources.
The error boundary component wraps the notifications component.
It is expected that after the connection fails, will show a custom component that says there is a problem along with a button to retry the connection.
However, the behavior of the error boundary is: It pauses while the popover is open (nothing is shown) and then just a few moments before it closes, it displays the custom component to retry the connection and the popover finishes closing.
export default function PopOver() {
const [anchorNotificationEl, setAnchorNotificationEl] = React.useState(null);
const popoverNotiRef = React.useRef(null);
const isNotificationOpen = Boolean(anchorNotificationEl);
const handleClick = (event) => {
setAnchorNotificationEl(event.currentTarget);
};
const handleNotificationClose = () => {
setAnchorNotificationEl(null);
};
const notificationId = 'notification-popover';
return (
<>
<IconButtonCustom
aria-label="show 17 new notifications"
color="inherit"
onClick={handleClick}
id="notifications-icon"
>
<Badge
badgeContent={0}
color="secondary"
>
<NotificationsIcon
fontSize="large"
/>
</Badge>
</IconButtonCustom>
<Popover
ref={popoverNotiRef}
id={notificationId}
open={isNotificationOpen}
anchorEl={anchorNotificationEl}
onClose={handleNotificationClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center',
}}
elevation={1}
>
<Notifications popoverRef={popoverNotiRef.current} />
</Popover>
</>
);
}
class ErrorBoundary extends React.Component <Props, State> {
constructor(props) {
super(props);
this.state = { error: null };
}
static getDerivedStateFromError(error): State {
return { error };
}
componentDidCatch(error) {
this.setState({ error });
}
render() {
const { children, fallback } = this.props;
const { error } = this.state;
if (error) {
if (fallback) {
return fallback;
}
return <p>Something is wrong</p>;
}
return children;
}
}
export default function NotificationWrapper({ popoverRef }: NWProps) {
const [queryReference, loadQuery] = useQueryLoader(NotificationsQuery);
useEffect(() => {
loadQuery({
count: 5,
}, { fetchPolicy: 'store-and-network' });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<ErrorBoundary fallback={<RetryComponent loadQuery={loadQuery}/>}>
{queryReference && (
<Suspense fallback={<Loading />}>
<NotificationsIntermediate queryRef={queryReference} popoverRef={popoverRef} />
</Suspense>
)}
</ErrorBoundary>
</>
);
}

bind this - call delete item

I would like to ask more experienced coleagues for help in understanding below piece of code and make it working in my App.
The general goal is to invoke an REDUX action from my button, which will delete an item from a database.
Here is a piece of code that works for me:
<MenuItem onClick={this.props.deleteTrip.bind(this, trip.id)}>
deleteTrip
</MenuItem>
and this code is a part of this file:
class Trips extends Component {
state = {
anchorEl: null,
dialogOpen: false
};
//integracja z reduxem
static propTypes = {
trips: PropTypes.array.isRequired
};
//
handleClose = () => {
this.setState({
anchorEl: null
});
};
handleClick = event => {
this.setState({
anchorEl: event.currentTarget
});
};
render() {
const { label, status, trips } = this.props;
const { anchorEl, dialogOpen } = this.state;
const TripsFiltered = [];
return (
<Fragment>
<List>
<ListItemLink>
<ListItemAvatar>
<Avatar>
<WorkIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={trip.remarks}
secondary={
trip.startDate + "-" + trip.endDate + ", " + trip.place
}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
aria-label="more"
aria-controls="simple-menu"
aria-haspopup="true"
onClick={this.handleClick}
>
<MoreIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={this.handleClose}
>
<MenuItem onClick={this.handleClose}>editTrip</MenuItem>
<MenuItem
onClick={this.props.deleteTrip.bind(this, trip.id)}>
deleteTrip
</MenuItem>
</Menu>
</ListItemSecondaryAction>
</ListItemLink>
</List>
</Fragment>
);
}
}
const mapStateToProps = state => ({
trips: state.tripsReducer.trips
});
export default connect(
mapStateToProps,
{ deleteTrip }
)(Trips);
But the case for me is that before deleting a Trip from database I want to show to the user a warning message so I modified the code like that:
class Trips extends Component {
state = {
anchorEl: null,
dialogOpen: false
};
//integracja z reduxem
static propTypes = {
trips: PropTypes.array.isRequired
};
//
handleOpenWarning = () => {
this.setState({
dialogOpen: true
});
};
handleCloseWarning = () => {
this.setState({
dialogOpen: false
});
};
handleDeleteTrip = () => {
this.setState({
dialogOpen: false
});
this.props.deleteTrip.bind(this, trip.id);
};
handleClose = () => {
this.setState({
anchorEl: null
});
};
handleClick = event => {
this.setState({
anchorEl: event.currentTarget
});
};
render() {
const { label, status, trips } = this.props;
const { anchorEl, dialogOpen } = this.state;
const TripsFiltered = [];
return (
<Fragment>
<List>
<ListItemLink>
<ListItemAvatar>
<Avatar>
<WorkIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={trip.remarks}
secondary={
trip.startDate + "-" + trip.endDate + ", " + trip.place
}
/>
<ListItemSecondaryAction>
<IconButton
edge="end"
aria-label="more"
aria-controls="simple-menu"
aria-haspopup="true"
onClick={this.handleClick}
>
<MoreIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={this.handleClose}
>
<MenuItem onClick={this.handleClose}>editTrip</MenuItem>
<MenuItem
onClick={this.handleOpenWarning}
>
deleteTrip
</MenuItem>
</Menu>
</ListItemSecondaryAction>
</ListItemLink>
</List>
<Dialog
open={dialogOpen}
onClose={this.handleCloseWarning}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Selected Trip will be deleted."}
</DialogTitle>
<DialogActions>
<Button onClick={this.handleCloseWarning} color="primary">
Cancel
</Button>
<Button onClick={this.handleDeleteTrip} color="primary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
}
const mapStateToProps = state => ({
trips: state.tripsReducer.trips
});
export default connect(
mapStateToProps,
{ deleteTrip }
)(Trips);
So after that modification I call {this.props.deleteTrip.bind(this, trip.id)} from handleDeleteTrip() function instead from onClick event directly. But when I do that I got:
./src/Trips/Trips.js
Line 57: 'trip' is not defined no-undef
In fact I dont understand to much this piece of code
{this.props.deleteTrip.bind(this, trip.id)}, so Im not sure how to properly use it in my arrow function to get this working.
in handleDeleteTrip(). there is no definition of the trip
handleDeleteTrip = () => {
this.setState({
dialogOpen: false
});
this.props.deleteTrip.bind(this, trip.id);
};
so you should pass the trip to the argument.
like this
handleDeleteTrip = (trip) => {
this.setState({
dialogOpen: false
});
this.props.deleteTrip.bind(this, trip.id);
};
...
<Button onClick={(e) => this.handleDeleteTrip(trip)} color="primary" autoFocus>
Delete
</Button>

How to change an array element in state in a React

I am trying to change an element of an array using the handleToggle method, but an error occurs, what am I doing wrong, and why should I always return a new array in the React?
Child component:
function Todolist(props) {
const todoItem = props.todos.map((todo, index) =>
<ListItem key={todo.id} dense button>
<ListItemIcon>
<Checkbox checked={todo.completed} onChange={props.onChange(todo.id)} edge="start"/>
</ListItemIcon>
<ListItemText primary={todo.title} />
<ListItemSecondaryAction>
<IconButton edge="end" aria-label="comments"></IconButton>
</ListItemSecondaryAction>
</ListItem>
)
return (
<div>
{todoItem}
</div>
)
}
Parent component:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
todos: [
{
title: 'Learn React',
id: Math.random(),
completed: true
}
]
}
};
handleChange = (evt) => {
this.setState({
value: evt.target.value
})
};
handleSubmit = (evt) => {
evt.preventDefault();
const todos = [...this.state.todos];
todos.push({
title: this.state.value,
id: Math.random(),
completed: false
});
this.setState(state => ({
todos,
value: ''
}))
};
handleToggle = (id) => {
const todos = [...this.state.todos];
todos.map(todo => {
if (todo.id === id) {
return todo.completed = !todo.completed
} else {
return todo
}
});
this.setState(state => ({
todos
}))
};
render() {
return (
<div className="App">
<Grid className="Header" justify="center" container>
<Grid item xs={11}>
<h1 className="Title">My to-do react app</h1>
<FormBox value={this.state.value} onSubmit={this.handleSubmit} onChange={this.handleChange}/>
</Grid>
</Grid>
<TodoList todos={this.state.todos} onChange={this.handleToggle} />
</div>
);
}
}
Text of error:
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
You are executing onChange immediately
onChange={props.onChange(todo.id)}
Wrap the callback in an arrow function
onChange={() => props.onChange(todo.id)}
Also your handleToggle isn't changing the values correctly (mutating), try like this
handleToggle = (id) => {
const todos = [...this.state.todos];
this.setState({ todos : todos.map(todo => ({
...todo,
completed : todo.id === id ? !todo.completed : todo.completed
}) }))
}

How to call child component method from parent

In my Reactjs app, I need to have a parent component (a wizard) named Wizard.js and a number of child components (steps of the wizard) named PrimaryForm.js, SecondaryForm.js etc. They all are Class based components with some local validation functions.
Previous and Next buttons to advance the steps, reside in the Wizard.js.
To advance the next step of the wizard, I'm trying to call a method from PrimaryForm. I checked similar questions in Stackoverflow; tried using ref or forwardRef, but I could not make it work. I currently receive "TypeError: Cannot read property 'handleCheckServer' of null" error.
Below are my parent and child classes. Any help about what I would be doing wrong is appreciated.
Wizard.js:
import React, { Component } from 'react';
...
const getSteps = () => {
return [
'Info',
'Source Details',
'Target Details',
'Configuration'
];
}
class Wizard extends Component {
constructor(props) {
super(props);
this.firstRef = React.createRef();
this.handleNext = this.handleNext.bind(this);
this.state = {
activeStep: 1,
}
}
componentDidMount() {}
handleNext = () => {
if (this.state.activeStep === 1) {
this.firstRef.current.handleCheckServer(); <<<<<<<<<<<<<<<<< This is where I try to call child method
}
this.setState(state => ({
activeStep: state.activeStep + 1,
}));
};
handleBack = () => {
this.setState(state => ({
activeStep: state.activeStep - 1,
}));
};
handleReset = () => {
this.setState({
activeStep: 0,
});
};
render() {
const steps = getSteps();
const currentPath = this.props.location.pathname;
const { classes } = this.props;
return (
<React.Fragment>
<CssBaseline />
<Topbar currentPath={currentPath} />
<div className={classes.root}>
<Grid container spacing={2} justify="center" direction="row">
<Grid container spacing={2} className={classes.grid} justify="center" direction="row">
<Grid item xs={12}>
<div className={classes.topBar}>
<div className={classes.block}>
<Typography variant="h6" gutterBottom>Wizard</Typography>
<Typography variant="body1">Follow the wizard steps to create a configuration.</Typography>
</div>
</div>
</Grid>
</Grid>
<Grid container spacing={2} alignItems="center" justify="center" className={classes.grid}>
<Grid item xs={12}>
<div className={classes.stepContainer}>
<div className={classes.bigContainer}>
<Stepper classes={{ root: classes.stepper }} activeStep={this.state.activeStep} alternativeLabel>
{steps.map(label => {
return (
<Step key={label}>
<StepLabel>{label}</StepLabel>
</Step>
);
})}
</Stepper>
</div>
<PrimaryForm ref={this.firstRef} />
</div>
</Grid>
</Grid>
<Grid container spacing={2} className={classes.grid}>
<Grid item xs={12}>
<div className={classes.flexBar}>
<Tooltip title="Back to previous step">
<div>
<Button variant="contained"
disabled={(this.state.activeStep === 0)}
className={classes.actionButton}
onClick={this.handleBack}
size='large'>
<BackIcon className={classes.rightIcon} />Back
</Button>
</div>
</Tooltip>
<Tooltip title="Proceed the next step">
<div>
<Button
variant="contained" className={classes.actionButton}
color="primary"
size='large'
disabled={!(!this.state.isFormValid || this.state.isTestWaiting)}
onClick={this.handleNext}>
<ForwardIcon className={this.props.classes.rightIcon}/>Next</Button>
</div>
</Tooltip>
<Tooltip title="Cancel creating new configuration">
<Button variant="contained" color="default" className={classes.actionButton}
component={Link} to={'/configs'} style={{ marginLeft: 'auto' }}>
<CancelIcon className={classes.rightIcon} />Cancel
</Button>
</Tooltip>
</div>
</Grid>
</Grid>
</Grid>
</div>
</React.Fragment>
)
}
}
export default withRouter(withStyles(styles)(Wizard));
PrimaryForm.js:
import React, { Component } from 'react';
...
class PrimaryForm extends Component {
constructor(props) {
super(props);
this.handleCheckServer = this.handleCheckServer.bind(this);
this.state = {
hostname: {
value: "localhost",
isError: false,
errorText: "",
},
serverIp: {
value: "127.0.0.1",
isError: false,
errorText: "",
},
isFormValid: true,
isTestValid: true,
testErrorMessage: "",
isTestWaiting: false,
};
}
componentDidMount() { }
handleCheckServer() {
alert('Alert from Child. Server check will be done here');
}
evaluateFormValid = (prevState) => {
return ((prevState.hostname.value !== "" && !prevState.hostname.isError) &&
(prevState.serverIp.value !== "" && !prevState.serverIp.isError));
};
handleChange = event => {
var valResult;
switch (event.target.id) {
case 'hostname':
valResult = PrimaryFormValidator.validateHostname(event.target.value, event.target.labels[0].textContent);
this.setState({
...this.state,
hostname:
{
value: event.target.value,
isError: valResult.isError,
errorText: valResult.errorText,
},
});
break;
case 'serverIp':
valResult = PrimaryFormValidator.validateIpAddress(event.target.value, event.target.labels[0].textContent);
this.setState({
...this.state,
serverIp:
{
value: event.target.value,
isError: valResult.isError,
errorText: valResult.errorText,
}
});
break;
default:
}
this.setState(prevState => ({
...prevState,
isFormValid: this.evaluateFormValid(prevState),
}));
}
render() {
const { classes } = this.props;
return (
<React.Fragment>
<div className={classes.bigContainer}>
<Paper className={classes.paper}>
<div>
<div>
<Typography variant="subtitle1" gutterBottom className={classes.subtitle1} color='secondary'>
Primary System
</Typography>
<Typography variant="body1" gutterBottom>
Information related with the primary system.
</Typography>
</div>
<div className={classes.bigContainer}>
<form className={classes.formArea}>
<TextField className={classes.formControl}
id="hostname"
label="FQDN Hostname *"
onChange={this.handleChange}
value={this.state.hostname.value}
error={this.state.hostname.isError}
helperText={this.state.hostname.errorText}
variant="outlined" autoComplete="off" />
<TextField className={classes.formControl}
id="serverIp"
label="Server Ip Address *"
onChange={this.handleChange}
value={this.state.serverIp.value}
error={this.state.serverIp.isError}
helperText={this.state.serverIp.errorText}
variant="outlined" autoComplete="off" />
</form>
</div>
</div>
</Paper>
</div>
</React.Fragment>
)
}
}
export default withRouter(withStyles(styles)(PrimaryForm));
(ps: I would like to solve this without another framework like Redux, etc if possible)
Example in Typescript.
The idea is that the parent passes its callback to the child. The child calls the parent's callback supplying its own e.g. child callback as the argument. The parent stores what it got (child callback) in a class member variable and calls it later.
import * as React from 'react'
interface ICallback {
(num: number): string
}
type ChildProps = {
parent_callback: (f: ICallback) => void;
}
class Child extends React.Component {
constructor(props: ChildProps) {
super(props);
props.parent_callback(this.childCallback);
}
childCallback: ICallback = (num: number) => {
if (num == 5) return "hello";
return "bye";
}
render() {
return (
<>
<div>Child</div>
</>
)
}
}
class Parent extends React.Component {
readonly state = { msg: "<not yet set>" };
letChildRegisterItsCallback = (fun: ICallback) => {
this.m_ChildCallback = fun;
}
callChildCallback() {
const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
console.log("Child callback returned string: " + str);
return str;
}
componentDidMount() {
this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
}
render() {
return (
<>
<Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
<div>{this.state.msg}</div>
</>
)
}
m_ChildCallback: ICallback | undefined = undefined;
}
P.S.
The same code in Javascript. The only difference is that interface, type, readonly and type annotations are taken out. Pasting into here confirms it's a valid ES2015 stage-2 code.
class Child extends React.Component {
constructor(props) {
super(props);
props.parent_callback(this.childCallback);
}
childCallback = (num) => {
if (num == 5) return "hello";
return "bye";
}
render() {
return (
<>
<div>Child</div>
</>
)
}
}
class Parent extends React.Component {
state = { msg: "<not yet set>" };
letChildRegisterItsCallback = (fun) => {
this.m_ChildCallback = fun;
}
callChildCallback() {
const str = this.m_ChildCallback? this.m_ChildCallback(5) : "<callback not set>";
console.log("Child callback returned string: " + str);
return str;
}
componentDidMount() {
this.setState((prevState) => { return {...prevState, msg: this.callChildCallback()} });
}
render() {
return (
<>
<Child {...{ parent_callback: this.letChildRegisterItsCallback }} />
<div>{this.state.msg}</div>
</>
)
}
m_ChildCallback = undefined;
}

Isolating a function when data is mapped in react

I have data being mapped as a repeater. But I need to isolate the opening function (It's an accordion). I'm still learning my way through React. Basically, the accordions load with the state for open: false Once the ListItem is clicked, the HandleClick function toggles the state to open: true. A simple concept, I just need to isolate it so that it works independently. Whereas right now they all open and close at the same time.
Here is the state in a constructor and function
constructor(props) {
super(props);
this.state = {
open: true,
};
}
handleClick = () => { this.setState({ open: !this.state.open }); };
Here is my mapping script in ReactJS
{LicenseItems.map((item, index) => (
<div key={index}>
<ListItem
divider
button
onClick={this.handleClick}>
<ListItemText primary={<CMLabel>{item.accordion_name}</CMLabel>}/>
</ListItem>
<Collapse
in={!this.state.open}
timeout="auto"
unmountOnExit>
{item.content}
</Collapse>
</div>
))}
The in dictates whether it is open or not per MaterialUI-Next
Thanks in advance guys!
Not very pretty, but something like this should work:
constructor(props) {
super(props);
this.state = {
open: {},
};
}
handleClick = (idx) => {
this.setState(state => ({open: { [idx]: !state.open[idx]} }))
}
// in render
{LicenseItems.map((item, index) => (
<div key={index}>
<ListItem
divider
button
onClick={() => this.handleClick(index)}>
<ListItemText primary={<CMLabel>{item.accordion_name}</CMLabel>}/>
</ListItem>
<Collapse
in={!this.state.open[index]}
timeout="auto"
unmountOnExit>
{item.content}
</Collapse>
</div>
))}
It would be better to create separate Components for that, which have their own open state.
You should create two components for that:
Accordions.js
import React from 'react'
import Accordion from './Accordion'
const Accordions = props => {
return (
props.LicenseItems.map((item, index) => (
<Accordion key={index} item={item} />
))
);
}
export default Accordions;
Accordion.js
import React, { Component } from 'react'
class Accordion extends Component {
constructor(props) {
super(props);
this.state = {
open: true,
};
}
handleClick = () => { this.setState({ open: !this.state.open }); };
render() {
return (
<div>
<ListItem
divider
button
onClick={this.handleClick}>
<ListItemText primary={<CMLabel>{this.props.item.accordion_name}</CMLabel>}/>
</ListItem>
<Collapse
in={!this.state.open}
timeout="auto"
unmountOnExit>
{this.props.item.content}
</Collapse>
</div>
)
}
}
export default Accordion;

Categories