ReactJS: array of compoents issue - javascript

I have a Main.js page that has one button: when you click it it adds a Block component to an array and to the page. You can add as many Block components as you want. Each Block component has a "delete" button, that will remove the Block from the array and from the page.
Menu.js:
import React from 'react';
import './Menu.css';
import Block from './Block.js';
import './Block.css';
export default class Menu extends React.Component {
constructor(props) {
super(props);
this.state = { value: '', blocksArray: [] };
this.addBlock = this.addBlock.bind(this);
this.removeBlock = this.removeBlock.bind(this);
this.blocks = [];
}
addBlock() {
this.blocks.push({ title: 'Section title' + this.blocks.length, content: 'Content' + this.blocks.length });
this.setState({ value: '', blocksArray: this.blocks });
}
removeBlock(index) {
this.blocks.splice(index, 1);
this.setState({ value: '', blocksArray: this.blocks })
}
renderBlocks = () => {
return (
this.state.blocksArray.map((block, index) =>
<Block
remove={() => this.removeBlock(index)}
key={index}
title={block.title}
content={block.content}
/>
)
)
}
render() {
return (
<div>
<div className="Menu">
<header className="Menu-header">
<button className="Menu-button" onClick={ () => this.addBlock() }>Add block</button>
</header>
</div>
<div>
{ this.renderBlocks() }
</div>
</div>
);
}
}
Block.js (version 1)
import React from 'react';
import './Block.css';
class Block extends React.Component {
constructor(props) {
super(props);
this.state = {
title: props.title,
content: props.content,
remove: props.remove
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({[event.target.name]: event.target.value});
}
handleSubmit(event) {
//alert('A name was submitted: ' + this.state.title);
event.preventDefault();
}
render() {
return (
<div className="Block-container">
<form onSubmit={this.handleSubmit}>
<div className="Block-title">
<label>
Title:
<input type="text" name="title" value={this.props.title} onChange={this.handleChange} />
</label>
</div>
<div className="Block-content">
<label>
Content:
<input type="text" name="content" value={this.props.content} onChange={this.handleChange} />
</label>
</div>
<input type="submit" value="Save" />
<input type="button" value="Delete" onClick= { () => this.state.remove() } />
</form>
</div>
);
}
}
export default Block;
The issue: I found myself stuck with 2 situations and neither works properly.
First non working solution for Block.js:
<input type="text" name="title" value={this.props.title} onChange={this.handleChange} />
<input type="text" name="content" value={this.props.content} onChange={this.handleChange} />
If I use value={this.props.content} and value={this.props.title} when I push the delete button on the Block it works but I can't edit the text in the field since its value is always retrieved from the props.
Second non working solution for Block.js:
<input type="text" name="title" value={this.state.title} onChange={this.handleChange} />
<input type="text" name="content" value={this.state.content} onChange={this.handleChange} />
If I use value={this.state.content} and value={this.state.title} I can edit the text fields and when I push the delete button on the Block it removes properly the component from the array, but the text displayed in the fields is wrong (it's as if it's always popping only the last component from the array). Let me explain with a few screenshots.
Let's say I added 4 Block components, as follow:
Then I click on the delete button of the Block with "Section title1" / "Content1", as this screenshot:
It apparently removes the right element in the array, but for some reason I get the wrong text in the component:
Array console.log:
0: Object { title: "Section title0", content: "Content0" }
1: Object { title: "Section title2", content: "Content2" }
2: Object { title: "Section title3", content: "Content3" }
Displayed text:
I'm obviously missing something and I have been stuck for a while. Can someone explain what is wrong?

I think the problem is you are setting index as the key for each Block.
Origin keys are [0, 1, 2, 3]. When you remove Section title1, new render will produce keys [0, 1, 2]. So React assumes that element with keys [0, 1, 2] are not changed and key 3 is removed. So it removed the last one.
Try to use an unique property for the key.
You can read more here: https://reactjs.org/docs/reconciliation.html#keys

Your change handler needs to be operating on the state in the parent component where the title/content are coming from. The values shown in the Blocks are being read from the Menu's state, so while editing the block data changes its own internal state, the values coming from the menu to the block as props are staying the same because the internal state is not being fed back.
You could write a function to edit the arrays in the Menu state in place:
this.editBlock = this.editBlock.bind(this);
...
editBlock(index, newBlock) {
let blocks = Array.from(this.state.blocksArray);
blocks[index] = newBlock;
this.setState({
blocksArray: blocks
})
}
and then pass that to the Block as props and call it when the change event fires:
<Block
remove={() => this.removeBlock(index)}
key={index}
title={block.title}
content={block.content}
index={index}
editBlock={this.editBlock}
/>
handleChange(event) {
this.setState({[event.target.name]: event.target.value}, () => {
this.props.editBlock(this.props.index, { title: this.state.title, content: this.state.content})
});
}
Working demo here.

Related

Issue with unique key props for child elements

I have a generic Todo List built in React. Each task from user input is stored in tasks array declared in parent component <ReactTodoApp />. Tasks are rendered in child component <TodoList />. A unique key is assigned to each task in DOM element <label />. When inspecting dev tools unique ids are generating, however error is still present.
Would anyone know why I am still getting the "unique key prop" error?
Link to working application: https://codesandbox.io/s/todo-list-34udn?file=/src/App.js
JS file:
import React, { Component } from "react";
import "./styles.css";
export default class ReactTodoApp extends Component {
constructor(props) {
super(props);
this.state = {
//container for new task
input: "",
tasks: []
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleRemove = this.handleRemove.bind(this);
}
handleChange(event) {
this.setState({ input: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
//condition for empty empty
if (!this.state.input) {
return;
}
//declare object to store
const newTask = {
input: this.state.input,
id: 1 + Math.random()
};
//request update to current tasks state
this.setState((state) => ({
tasks: state.tasks.concat(newTask),
input: ""
}));
}
//updater function to remove task
handleRemove(props) {
//create new task list
const newTasksList = this.state.tasks;
//remove selected item from new task list
newTasksList.splice(props, 1);
//update state for tasks
this.setState({ tasks: newTasksList });
}
render() {
return (
<div>
<h1>React Todo</h1>
<form onSubmit={this.handleSubmit} className="add-item">
<input
type="text"
value={this.state.input}
onChange={this.handleChange}
className="add-item__input"
placeholder="new item"
/>
<button type="submit" className="submit">
add item
</button>
</form>
<TodoList tasks={this.state.tasks} handleRemove={this.handleRemove} />
</div>
);
}
}
class TodoList extends React.Component {
render() {
return (
<div className="list-container">
{this.props.tasks.map((task) => (
<label keys={task.id} className="item-container">
<input type="checkbox" />
<p className="item__text">{task.input}</p>
<button onClick={this.props.handleRemove} className="remove-button">
x
</button>
<span className="custom-checkbox" />
</label>
))}
</div>
);
}
}
Just change keys={task.id} to key={task.id}

Why does my React form auto-refresh the page even if I put "event.preventDefault()" on handleSubmit?

I have two files which work together to render things. The first is App.js, which first renders Form.js. The form will then collect information, which on submission, changes the Form state and calls a function from App.js. This function is called "createProject." Calling "createProject" in Form.js "handleSubmit" makes the page auto-refresh. However, if I remove "createProject" from handleSubmit, the page does not auto-refresh. Here are the two files.
import React, { Component } from "react";
import Project from "./components/Project.js"
import Form from "./Form.js";
class App extends Component {
constructor(props) {
super(props);
this.state = {
projectList: [],
myProjects: [],
userList: [],
submitted: false
};
this.createProject = this.createProject.bind(this);
}
createProject(title, desc, langs, len, exp) {
this.setState({
projectList: this.state.projectList.push([
{
title : title,
description : desc,
language : langs,
length : len,
experience : exp
}
]),
submitted : true
});
}
deleteProject(title) {
const projects = this.state.projectList.filter(
p => p.title !== title
);
this.setState({projects});
}
render() {
let info;
if (this.state.submitted) {
info = (
<div>
<p>cccc</p>
</div>
);
} else {
info = (
<br/>
);
}
return(
<div>
<Form/>
{info}
{this.state.projectList.map((params) =>
<Project {...params}/>)}
</div>
);
}
}
export default App;
import React from "react";
import createProject from "./App.js"
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
title: "",
description: "",
language: "",
length: 0,
experience: "",
submitted: false
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleInputChange = this.handleInputChange.bind(this);
}
handleSubmit(event) {
this.setState({
submitted: true
})
createProject(
this.state.title,
this.state.description,
this.state.language,
this.state.length,
this.state.experience
)
event.preventDefault();
}
handleInputChange(event) {
const target = event.target;
const value = target.value;
const name = target.name;
this.setState({
[name]: value
});
}
render() {
let info;
if (this.state.submitted) {
info = (
<div>
<h1>{this.state.title}</h1>
<p>{this.state.description}</p>
<p>{this.state.language}</p>
<p>{this.state.length}</p>
<p>{this.state.experience}</p>
</div>
);
} else {
info = <br/>;
}
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
Title:
<input
name="title"
type="textbox"
checked={this.state.title}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Description:
<input
name="description"
type="textbox"
checked={this.state.description}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Language:
<input
name="language"
type="textbox"
checked={this.state.language}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Length:
<input
name="length"
type="number"
checked={this.state.length}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Experience:
<input
name="experience"
type="textbox"
checked={this.state.experience}
onChange={this.handleInputChange} />
</label>
<br />
<input type="submit" value="Submit" />
</form>
{info}
</div>
);
}
}
export default Form;
I've also tried adding "new" to the "createProject" in handleSubmit, and while that does stop the auto-refresh, it will not call the createProject function. (Or maybe it does, but none of the code in the createProject function seems to be run.) Can anyone help with preventing this auto refresh while also allowing App's createProject function to run properly?
The page auto refreshes because execution never gets to your event.PreventDefault() line. This is due to an error encountered when react tries to evaluate createProject. To fix this, correct handleSubmit like so.
handleSubmit(event) {
event.preventDefault(); // moved up in execution.
this.setState({
submitted: true
})
createProject(
this.state.title,
this.state.description,
this.state.language,
this.state.length,
this.state.experience
)
}
Notice that moving event.PreventDefault() to the top of your handleSubmit(event) function just before this.setState line prevents default form behaviour on submit.
You however get an error because App.js doesn't export a function called createProject. To maintain the createProject within App instance, you need to pass it as a prop which you can then reference as this.props.createProject.
See this answer on how to do call a Parent method in ReactJS.

How to get values from multiple Textareas in React

I have a form that has more than 1 textarea(3 to exact). The issue is that, if I type something in 1 textarea then the other textareas are also filled and updated. And I dont' want that. I'm using an onChange handler to make them controlled components but they're not behaving as expected. I've looked online but couldn't find a solution! How do I make them update individually on change/type?
React Textarea component
const Textarea = ({ labelText, placeholder, value, name, onChange }) => {
return (
<div className="textarea-panel">
<label>{labelText}</label>
<textarea
style={{ display: 'block' }}
cols="60"
rows="5"
placeholder={placeholder}
value={value}
name={name}
onChange={onChange}
/>
</div>
);
}
export default Textarea;
React Form component
import Textbox from './Textbox';
export class Form extends Component {
constructor(props) {
super(props);
this.state = {
data: {
career: '',
experience: '',
additionalInformation: '',
},
};
this.handleTextareaChange = this.handleTextareaChange.bind(this);
}
handleTextareaChange(event) {
this.setState({
data: {
[event.target.name]: event.target.value,
},
});
}
render() {
return (
<form>
<ul>
<li>
<Textbox
labelText="Write your career "
value={this.state.data.career}
placeholder="e.g. extra information"
name="career"
onChange={this.handleTextareaChange}
/>
<span>{this.state.data.career}</span>
</li>
<li>
<Textbox
labelText="Write your experience"
value={this.state.data.experience}
placeholder="e.g. extra information"
name="experience"
onChange={this.handleTextareaChange}
/>
<span>{this.state.data.experience}</span>
</li>
<li>
<Textbox
labelText="Additional information"
value={this.state.data.additionalInformation}
placeholder="e.g. extra information"
name="additionalInformation"
onChange={this.handleTextareaChange}
/>
<span>{this.state.data.additionalInformation}</span>
</li>
</ul>
</form>
);
}
}
export default Form;
You're overriding the entire state with a new key on the data object. Every time the onChange handler is called the entire state gets overriden with a new data object with the current textbox's name as the key. Instead just update the particluar key on the data like this
Try it here : Codesandbox
handleTextareaChange(event) {
const { data } = this.state
data[event.target.name] = event.target.value
this.setState({
data
});
}
Hope this helps !

Passing ref up to parent in react

I have a little app that has an input and based on the search value, displays weather for a particular city. I'm stuck at a certain point though. The idea is that once you search a city, it hides the text input and search button and displays some weather info and another search button to search a new city. My issue is that I want to focus on the search box once I click to search again. I hope that makes sense. I read that the ideal way to do this is with refs. I wired it up like such:
class WeatherForm extends React.Component {
constructor(props) {
super(props);
this.city = React.createRef();
}
componentDidMount() {
this.props.passRefUpward(this.city);
this.city.current.focus();
}
render() {
if (this.props.isOpen) {
return (
<div className={style.weatherForm}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
)
} else {
return (
<div className={style.resetButton}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
);
}
}
}
With this I can pass that ref up to the parent to use in my search by using this.state.myRefs.current.value; It works great, but when I try to reference this.state.myRefs.current in a different function to use .focus(), it returns null.
resetSearch = () => {
console.log(this.state.myRefs.current); // <- returns null
this.setState({
isOpen: !this.state.isOpen,
details: [],
video: []
});
}
Is this because I'm hiding and showing different components based on the search click? I've read numerous posts on SO, but I still can't crack this. Any help is appreciated. I'll include the full code below. To see it in full here is the git repo: https://github.com/DanDeller/tinyWeather/blob/master/src/components/WeatherMain.js
class Weather extends React.Component {
constructor(props) {
super(props);
this.state = {
recentCities: [],
details: [],
isOpen: true,
myRefs: '',
video: '',
city: ''
};
this.updateInputValue = this.updateInputValue.bind(this);
this.getRefsFromChild = this.getRefsFromChild.bind(this);
this.resetSearch = this.resetSearch.bind(this);
this.getWeather = this.getWeather.bind(this);
}
updateInputValue = (e) => {
...
}
resetSearch = () => {
console.log(this.state.myRefs.current);
this.setState({
isOpen: !this.state.isOpen,
details: [],
video: []
});
}
getWeather = (e) => {
...
}
getRefsFromChild = (childRefs) => {
...
}
render() {
return (
<section className={style.container}>
<div className={style.weatherMain + ' ' + style.bodyText}>
<video key={this.state.video} className={style.video} loop autoPlay muted>
<source src={this.state.video} type="video/mp4">
</source>
Your browser does not support the video tag.
</video>
<div className={style.hold}>
<div className={style.weatherLeft}>
<WeatherForm
updateInputValue={this.updateInputValue}
getWeather={this.getWeather}
passRefUpward={this.getRefsFromChild}
resetSearch={this.resetSearch}
isOpen={this.state.isOpen}
/>
<WeatherList
details={this.state.details}
city={this.state.city}
isOpen={this.state.isOpen}
/>
</div>
<div className={style.weatherRight}>
<Sidebar
recentCities={this.state.recentCities}
/>
</div>
<div className={style.clear}></div>
</div>
</div>
</section>
);
}
}
class WeatherForm extends React.Component {
constructor(props) {
super(props);
this.city = React.createRef();
}
componentDidMount() {
this.props.passRefUpward(this.city);
this.city.current.focus();
}
render() {
if (this.props.isOpen) {
return (
<div className={style.weatherForm}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
)
} else {
return (
<div className={style.resetButton}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
);
}
}
}
export default Weather;
You try to achieve unmounted component from DOM, because of this you can not catch the reference. If you put this code your instead of render function of WeatherForm component, you can catch the reference. Because i just hide it, not remove from DOM.
render() {
return (
<div>
<div className={style.weatherForm}
style={this.props.isOpen ? {visibility:"initial"} :{visibility:"hidden"}}>
<form action='/' method='GET'>
<input
ref={this.city}
onChange={this.props.updateInputValue}
type='text'
placeholder='Search city'
/>
<input
onClick={e => this.props.getWeather(e)}
type='submit'
value='Search'
/>
</form>
</div>
<div className={style.resetButton} style={this.props.isOpen ? {visibility:"hidden"} :{visibility:"initial"}}>
<p>Seach another city?</p>
<button
onClick={this.props.resetSearch}>Search
</button>
</div>
</div>
)
}
console.log(this.state.myRefs.current) returns null , because it's a reference to an input dom element which does not exists as currently Weather form is displaying Search another city along with a reset button.
In reset function state changes, which results in change of prop isOpen for WeatherForm component. Now, screen would be displaying the input field along with search button.
After component is updated ComponentDidUpdate lifecycle method is called.
Please add ComponentDidUpdate lifecycle method in WeatherForm and add ,
this.city.current.focus() in the body of method.
There is no need to pass reference of a dom element to the parent element as it is not consider as a good practise.
Edit 1 :-
Need to set input field in focus only if prop ( isOpen ) is true as we will get reference to the input field only if its mounted.
ComponentDidUpdate(){
if(this props.isOpen)
this.city.current.focus
}
Link to Lifecycle method :-
https://reactjs.org/docs/react-component.html#componentdidupdate
Hope this helps,
Cheers !!

React collect form properties and submit

I am new to React and Redux, hence, sorry for the dummy question.
I have a component:
export default class AddReminder extends Component {
render() {
return (
<div>
Name: <input name="name" />
Description: <input name="description" />
<button>
Add reminder
</button>
</div>
)
}
}
Also, I have an action located in different file what I want to call on button click.
export function addReminder(reminder) { ... }
So, I would like to create a reminder object with name and description properties and call an action. Also, I do not want to create a <form>, just a simple div. Can you please explain how can I do that?
If you don't want to use form element, you can store inputs values in state and on button click, pass state to addReminder func:
export default class AddReminder extends Component {
constructor() {
this.state = {
name: '',
description: ''
}
this.handleSubmit = this.handleSubmit.bind(this);
this.handleNameChange = this.handleNameChange.bind(this);
}
handleNameChange(e) {
this.setState({
name: e.target.value
});
}
handleDescChange(e) {
this.setState({
description: e.target.value
});
}
handleSubmit() {
addReminder(this.state);
}
render() {
return (
<div>
Name: <input name="name" value={this.state.name} onChange={handleNameChange} />
Description: <input name="description" value={this.state.description} onChange={handleDescChange} />
<button onClick={this.handleSubmit}>
Add reminder
</button>
</div>
)
}
}
But this solution is cumbersome, I think.Instead of using state, you can use form element, and inside of onSubmit callback, serialize it values to object and pass them to addReminder func:
// You should install `form-serialize` package from npm first.
import serialize from 'form-serialize';
// Note, we are using functional stateless component instead of class, because we don't need state.
function AddReminder() {
let form;
function handleSubmit(e) {
// Preventing default form behavior.
e.preventDefault();
// serializing form data to object
const data = serialize(form, { hash: true });
addReminder(data);
}
// Assigning form node to form variable
return (
<form ref={node => form = node} onSubmit={handleSubmit}>
Name: <input name="name" />
Description: <input name="description" />
<button type="submit">
Add reminder
</button>
</form>
);
}
class AddReminder extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
const parent = e.target.parentNode.children;
const { name, description } = parent;
dispatch(actionName({name.value, description.value}));
}
render() {
return (
<div>
Name: <input name="name" />
Description: <input name="description" />
<button onClick={this.handleClick}>
Add reminder
</button>
</div>
);
}
}
import {addReminder} from './addReminder';
export default class AddReminder extends Component {
render() {
addReminder() {
//call your action or do whatever
return {
name: this.refs.name.value,
description: this.refs.description.value
}
}
return (
<div>
Name: <input name="name" />
Description: <input name="description" />
<button onClick={addReminder.bind(this}>
Add reminder
</button>
</div>
)
}
}

Categories

Resources