How to change an array element in state in a React - javascript

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
}) }))
}

Related

How to handle null state exception for map in reactJs

I have this code which his basic todo list that I am trying to build in react
const Home = (props) => {
const classes = useStyles();
const [addNewTask, setAddNewTask] = useState(false);
const [formData, setFormData] = React.useState({ taskText: "" });
const dispatch = useDispatch();
useEffect(() => {
dispatch(getTasks());
}, [dispatch]);
const todos = useSelector((state) => state.todos);
const handleAddNew = () => {
setAddNewTask(addNewTask ? false : true);
};
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
console.log(formData);
dispatch(createTask(formData));
dispatch(getTasks())
};
return (
<Grow in>
<Container>
<Grid
className={classes.adjustTop}
container
justify="space-between"
alignItems="stretch"
spacing={3}
>
<Grid item xs={2} sm={2}></Grid>
<Grid item xs={8} sm={8}>
<Typography variant="h2">TODO LIST</Typography>
{todos==null?
<Typography>Loading...</Typography>
:
<>
{todos?.tasks?.map((todo) => (
<Task id={todo.task} todo={todo} />
))}
</>
}
<div style={{ textAlign: "center" }}>
{addNewTask ? (
<>
<Button
variant="contained"
size="large"
color="error"
style={{ marginTop: "10px" }}
onClick={handleAddNew}
>
Cancel Adding task
</Button>
<form style={{ paddingTop: "20px" }} onSubmit={handleSubmit}>
<Input
name="taskText"
label="Task Text"
handleChange={handleChange}
type="text"
placeholder="text"
/>
<br />
<Button type="submit">Submit</Button>
</form>
</>
) : (
<Button
variant="contained"
size="large"
color="primary"
style={{ marginTop: "10px" }}
onClick={handleAddNew}
>
+ Add new task
</Button>
)}
</div>
</Grid>
<Grid item xs={2} sm={2}></Grid>
</Grid>
</Container>
</Grow>
);
};
export default Home;
This is the reducer
import { FETCH_ALL,CREATE } from '../constants/actionTypes';
export default (state = {tasks:null}, action) => {
switch (action.type) {
case FETCH_ALL:
return {...state,tasks:action.payload,errors:null}
case CREATE:
return {...state,tasks:action.payload,errors:null}
default:
return state;
}
};
& actions
import { FETCH_ALL,CREATE} from '../constants/actionTypes';
import * as api from '../api/index.js';
export const getTasks = () => async (dispatch) => {
try {
const { data } = await api.fetchTasks();
console.log(data);
dispatch({ type: FETCH_ALL, payload: data });
} catch (error) {
console.log(error.message);
}
};
export const createTask = (taskText) => async (dispatch) => {
try {
const { data } = await api.createTask(taskText);
dispatch({type:CREATE,payload:data})
} catch (error) {
console.log(error.message);
}
};
I am able to add the data to database on submit using handleSubmit tot the database but the issue is after each submit its giving me an error TypeError: _todos$tasks.map is not a function
I tried to handle this by using ternary operator & rendering Loading text on null & also have used chaining operator,but still getting same error
You are trying to get todos from your redux store
const todos = useSelector((state) => state.todos);
But there is no todos in your state and plus you are updating another state tasks in your reducer:
export default (state = {tasks:null}, action) => {
switch (action.type) {
case FETCH_ALL:
return {...state,tasks:action.payload,errors:null}
case CREATE:
return {...state,tasks:action.payload,errors:null}
default:
return state;
}
};

onClick gets fired immediately on component mount using firestore and React, redux

I've got a component, that displays a project. For each project, there is a delete button, but for some reason, the delete buttons of all my projects get "pressed" immediately.
Why would this happen? I'm using redux and firestore. I think this has maybe something to do with the realtime listener, but don't have any problems with adding data to the database.
Projects.js
componentDidMount = () => {
this.props.projectActions.registerProjectListener();
};
renderProjects = () => {
const { projects } = this.props.projects;
const { classes, projectActions } = this.props;
return (
<Paper elevation={0} square={true} className={classes.projectPaper}>
<Box fontSize="h5.fontSize" textAlign="center">
Your Projects:
</Box>
{projects &&
projects.map(project => {
return <Project {...{ key: project.id, project, projectActions }}></Project>;
})}
<Grid container className={classes.grid} direction="row" justify="flex-end" alignItems="center">
<AddProject {...{ projectActions }}></AddProject>
</Grid>
</Paper>
);
};
Project.js
export class Project extends Component {
render() {
const { classes, project, projectActions } = this.props;
console.log(project.id);
return (
<Paper elevation={3} variant="outlined" className={classes.paper}>
<Grid container justify="space-between">
<Box fontSize="h6.fontSize">{project.name}</Box>
<IconButton onClick={projectActions.deleteProject(project.id)}> //This gets fired immediately for every project
<ClearIcon></ClearIcon>
</IconButton>
</Grid>
</Paper>
);
}
}
actions
export const deleteProject = id => {
return dispatch => {
console.log(db.collection("projects").doc(id));
// db.collection("projects")
// .doc(id)
// .delete();
dispatch({ type: ActionTypes.DELETE_PROJECT });
};
};
export const registerProjectListener = () => {
let projects = [];
return dispatch => {
db.collection("projects").onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type === "added") {
console.log(change.doc.data());
projects.push({ ...change.doc.data(), ...{ id: change.doc.id } });
} else if (change.type === "removed") {
// projects.filter(()=> )
}
});
dispatch({ type: ActionTypes.REGISTER_LISTENER, projects: projects });
});
};
};
You need to pass reference to a function. Update the IconButton component with the following.
<IconButton
onClick={() => projectActions.deleteProject(project.id)}>
<ClearIcon></ClearIcon>
</IconButton>

ReactJS: Rendering the state separately and not as an array

I ended up pulling off what I wanted. However, it's giving me an array of the state instead of rendering each one separately. This is probably very simple and I'm more than likely over-complicating it but hey, any help would be nice.
Here's what I currently am dealing with
And here's a better example: https://i.imgur.com/WLDkbOb.gif
And lastly here's probably the best overview: https://imgur.com/a/zintqTA
constructor(props) {
super(props);
this.state = {
data: [],
loading: false,
}
}
ws = new WebSocket(URL)
componentDidMount() {
this.ws.onopen = () => {
console.log('connected')
}
this.ws.onmessage = e => {
const tbox = JSON.parse(e.data);
if(tbox.data && tbox.data.length > 0){
this.setState({
data : this.state.data.concat(tbox.data[0]),
})
}
}
this.ws.onclose = () => {
console.log('disconnected')
this.setState({
ws: new WebSocket(URL),
})
}
}
render() {
let { data } = this.state;
const chatBox = data.map(item => {
return (
<List
key={item.id}
dataSource={this.state.data}
renderItem={item => (
<List.Item >
<List.Item.Meta
avatar={<Avatar size="large" icon="user" />}
title={<div>{item.user} {item.date}</div>}
description={item.message}
/>
</List.Item>
)}
>
</List>
)
})
return (
<div>
<div>
{chatBox}
</div>
I'm trying to loop through the state and render each message separately
I think you don't need to loop through this.state.data[] because you are already setting data source to antd <List> component. antd List component handles collection of objects for us.
This would be the code for rendring your this.state.data:
const chatBox = <List dataSource={this.state.data}
renderItem={item => (
<List.Item >
<List.Item.Meta
avatar={<Avatar size="large" icon="user" />}
title={<div>{item.user}
{item.date}</div>}
description={item.message}
/>
</List.Item>
)}
>
</List>;
you can have a look at these links :
https://stackblitz.com/run
https://ant.design/components/list/

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;
}

My child values are not changing while changing the state of the parent in React?

I am having a problem when I click on my child component, I want that when I select my component, all siblings get deselected but that does not happens, Well here is my source code so you can understand more the situation.
Here is my Parent component :
// All importations are right
import React, { Component } from "react";
import MenuItem from "./MenuItem";
import Grid from "../../../../components/Grid";
import {
faFolderOpen,
faCogs,
faFileInvoice,
faMale,
faList
} from "#fortawesome/pro-light-svg-icons";
import "./style.css";
class MenuLeft extends Component {
constructor(props) {
super(props);
this.state = {
menuItems: [
{ id: 1, item: "Données personelles", icon: faMale, isSelected: true },
{ id: 2, item: "Synthèse", icon: faFolderOpen, isSelected: false },
{ id: 3, item: "Historique", icon: faList, isSelected: false },
{ id: 4, item: "Analyse de portefeuille", icon: faFolderOpen, isSelected: false },
{ id: 5, item: "Description", icon: faFileInvoice, isSelected: false },
{ id: 6, item: "Documents", icon: faFolderOpen, isSelected: false },
{ id: 7, item: "Opérations", icon: faCogs, isSelected: false }
],
};
}
allDeselected = item => {
let menuItem = this.state.menuItems;
menuItem.forEach(function(elm) {
elm.isSelected = elm.id === item ? true : false;
});
this.setState({ menuItems: menuItem });
};
renderItems(items) {
console.log(items);
return items.map(item => (
<MenuItem
key={item.id}
id={item.id}
item={item.item}
icon={item.icon}
isSelected={item.isSelected}
allDeselected={this.allDeselected}
/>
));
}
render() {
console.log(this.state.menuItems);
return (
<div className="co-menu">
<div className="co-menu-left">
{this.renderItems(this.state.menuItems)}
</div>
</div>
);
}
}
export default MenuLeft;
and Here is my child component MenuItem :
class MenuItem extends Component {
constructor(props) {
super(props);
this.state = {
isSelected: this.props.isSelected
};
}
changeBackColor = key => {
alert(key);
this.props.allDeselected(key);
this.setState({ isSelected: !this.state.isSelected });
};
render() {
console.log("test" + this.props.id + " : " + this.state.isSelected);
return (
<Grid
onClick={() => this.changeBackColor(this.props.id)}
container
className={
this.state.isSelected
? "co-menu__backcolor co-menu__backcolor--selected"
: "co-menu__backcolor"
}
>
<Grid item xs={12} className="co-menu__icon pb-0">
<div className="co-menu__icon--centered">
<FontAwesomeIcon
icon={this.props.icon || ""}
size="3x"
className="co-menu__icon--color"
/>
</div>
</Grid>
<Grid item xs={12} className="co-menu__text pt-0">
<Text content={this.props.item} className="co-menu__text--color" />
</Grid>
<Grid item xs={12} className="pt-0" />
</Grid>
);
}
}
export default MenuItem;
You can see that when I click my element I execute allDeselected method in the parent and I see my data using console.log and here is what I get as a result when I click the third item for example :
Which is fine until now, but the state.isSelected does not change when doing this and here is the message I get in my child rendering method when showing the data :
You can see that the first one ( which was selected at the beggining ) has to be deselected but it maintain the same old isSelected value.
Is is a logic problem or what ?
I need help and any help would be much appreciated.
I would recommend you to make child components stateless and control everything state from a parent.
In the parent, you need to add another method.
onMenuClick(key) {
this.allDeselected(key)
this.setState((state) => {
const updatedMenu = state.menuItems.map(menu => {
if(menu.id === key)
return Object.assign({}, menu, { isSelected: !menu.isSelected });
return menu;
});
return {
...state,
menuItems: updatedMenu,
}
});
}
Then you can pass the data like this.
renderItems(items) {
return items.map(item => (
<MenuItem {...item} key={item.id} onMenuClick={this.onMenuClick}/>
));
}
now you can re-write your child like this to make it stateless.
const MenuItem = (props) => {
const componentClassName = this.state.isSelected
? "co-menu__backcolor co-menu__backcolor--selected"
: "co-menu__backcolor";
return (
<Grid
onClick={() => props.onMenuClick(props.id)}
container
className={componentClassName}
>
<Grid item xs={12} className="co-menu__icon pb-0">
<div className="co-menu__icon--centered">
<FontAwesomeIcon
icon={props.icon || ""}
size="3x"
className="co-menu__icon--color"
/>
</div>
</Grid>
<Grid item xs={12} className="co-menu__text pt-0">
<Text content={props.item} className="co-menu__text--color" />
</Grid>
<Grid item xs={12} className="pt-0" />
</Grid>
)
}

Categories