React Functional setState vs object difference - reactjs

sDoing a React course I faced with a strange situation deleting object from state. The course author used non-functional state update but I tried to make it functional and got an error. It would be great if you explain the principal difference in those two approaches.
To begin with state looks so:
{
items: {item11 : {_item1Props_}, item2: {item2Pros}},
order: {_some_suff_in_the_cart}
}
The following code performed for deleting item2:
deleteItem = key => {
const items= {...this.state.items, [key]: null};
this.setState({items});
}
All the components which use state updated with no errors (cart, view and so on).
Trying to make setState functional I got error in render methods of other components with the following code:
deleteItem = key => {
this.setState(prevState =>(
{
items:
{...prevState.items, [key]: null}
}
));
}
Even more the following code failed as well:
deleteItem = key => {
const items= {...this.state.items, [key]: null};
this.setState(prevState => ({items}));
}
So, there must be a principal difference in those methods?
Thank you in advance!
UPD components code:
There are two components using state:
TypeError: _this$props$details is null
Comes from the component which renders item:
class Item extends Component {
render() {
const {name, desc, price, status, image} = this.props.details;
const isAvailable = status === "available";
return (
<li className="menu-item">
<img src={image} alt={name}/>
<h3 className="item-name">
{name}
<span className="price">{formatPrice(price)}</span>
</h3>
<p>{desc}</p>
<button
disabled={!isAvailable}
onClick={()=>{this.props.addToOrder(this.props.index)}}
>
{isAvailable ? "Add To Cart" : "Sold Out"}
</button>
</li>
);
}
}
and
TypeError: this.props.item is null
from the component which is used to edit items:
class EditItemForm extends Component {
handleChange = (e) => {
const updatedItem = {
...this.props.item,
[e.target.name]: e.target.value
};
this.props.updateItem(this.props.index, updatedItem);
}
render() {
return (
<div className="item-edit">
<input
name="name"
type="text"
onChange={this.handleChange}
value={this.props.item.name}
/>
<input
name="price"
type="text"
onChange={this.handleChange}
value={this.props.item.price}
/>
<select
name="status"
value={this.props.item.status}
onChange={this.handleChange}
>
<option value="available">Available</option>
<option value="unavailable">Sold Out</option>
</select>
<textarea
name="desc"
type="text"
onChange={this.handleChange}
value={this.props.item.desc}
/>
<input
name="image"
type="text"
onChange={this.handleChange}
value={this.props.item.image}
/>
</div>
);
}
}
UPD2:
I logged the render() methods of mentioned components I found out that object version removes the property at all, so the null is not passed to the components meanwhile the functional preserves the null value when state is passed to the components.

Related

React: Select component not updating inside a dynamic form

I am attempting to create a dynamic form in which there are 2 text fields and one dropdown select. These fields can be added by clicking the "Add More.." button. The remove button removes a particular field set. After an npm start the code shows all elements normally, add, remove and input fields work as intended. However, the problem starts when the select is used. On selecting something, the app crashes and gives a white screen with the errors [tag:"formFields.map is not a function"] and [tag:"Consider adding an error boundary to your tree to customize error handling behavior."] I would appreciate any help that can resolve this. :)
P.S. I am learning react through building projects rather than the conventional method of sitting through hours of tutorials and figuring things out. I am grateful to any help that is offered to me.
import { useState } from "react";
function FoodPreferences(){
const [formFields, setFormFields] = useState([
{ name: '', age: '', food: '' }
])
const [foodState, setFoodState] = useState("dumpling");
const handleFormChange = (event, index) => {
let data = [...formFields];
data[index][event.target.name] = event.target.value;
setFormFields(data);
}
const handleSelectChange = (event, index) => {
const selectedFood = event.target.value
setFormFields(selectedFood)
}
const submit = (e) => {
e.preventDefault();
console.log(formFields, foodState)
}
const addFields = () => {
let object = {
name: '',
age: '',
food: ''
}
setFormFields([...formFields, object])
}
const removeFields = (index) => {
let data = [...formFields];
data.splice(index, 1)
setFormFields(data)
}
return (
<div className="App">
<form onSubmit={submit}>
{formFields.map((form, index) => {
return (
<div key={index}>
<input
name='name'
placeholder='Name'
onChange={event => handleFormChange(event, index)}
value={form.name}
/>
<input
name='age'
placeholder='Age'
onChange={event => handleFormChange(event, index)}
value={form.age}
/>
<select
className="custom-select"
value={form.food}
onChange={event => handleSelectChange(event,index)}
>
<option value="steak">Steak</option>
<option value="sandwich">Sandwich</option>
<option value="dumpling">Dumpling</option>
</select>
<button onClick={() => removeFields(index)}>Remove</button>
</div>
)
})}
</form>
<button onClick={addFields}>Add More..</button>
<br />
<button onClick={submit}>Submit</button>
</div>
);
}
export default FoodPreferences;
I have tried using the select component alone without looping it and it worked fine. The errors pop up when select component is placed under a map() for dynamic inputs (Adding or Removing Fields). I know that the error is either in the onChange part of my code for the select component or the handleSelectChange
import React, {useState} from 'react';
function FoodChoice() {
const \[foodState, setFoodState\] = useState("dumpling");
return (
<div className="container p-5">
<select
className="custom-select"
value={foodState}
onChange={(e) => {
const selectedFood = e.target.value;
setFoodState(selectedFood);
}}
>
<option value="steak">Steak</option>
<option value="sandwich">Sandwich</option>
<option value="dumpling">Dumpling</option>
</select>
{foodState}
</div>
);
}
export default FoodChoice;

React good practice - how to structure Parent and Child components including forms and where to keep state?

I have a question about reacts good practices when it comes to forms as to where to keep state (only parent etc). 2 approaches below:
Child (the form and input fields) has own onChange event and has state, only passes onsubmit the values back to parent and parent only passes down function to pass back those values
this approach Ive seen more. Child has no state (seems better as then the child is really just a presentational component and parent stores all the data and has state). However, then child passes 'back' the values on each onChange event (with each letter typed), ONLY to receive BACK the value it 'gave' to parent (since inputs need value most cases), which seems like back and forth data passing =waste?
I created 2 small versions to demonstrate. I would really appreciate opinions as to what way is better, since Im always wondering.
Thank you!!
class App extends React.Component {
state = {
name: "",
country: "",
result: ""
}
onChange = (key, val) => {
this.setState({
[key]: val
})
}
onSubmit = () => {
this.state({
result: this.state.name + " is from " + this.state.country
})
}
render() {
return (
<div>
<Child onChange={this.onChange} name={this.state.name} country={this.state.country}/>
<h1>{this.state.result}</h1>
</div>
)
}
}
const Child = ({onChange, onSubmit, name, country}) => {
return (
<form onSubmit={onSubmit}>
<input name="name" placeholder="name" value={name}
onChange={({target: {name, value}}) => onChange(name, value)}/>
<input name="country" placeholder="country" value={country}
onChange={({target: {name, value}}) => onChange(name, value)}/>
<input type="submit"/>
</form>
)
};
2nd version, where I keep state inside the child
class App extends React.Component {
state = {
result: ""
};
onSubmit = (event, name, country) => {
event.preventDefault();
this.setState({
result: name + " is from " + country
})
};
render() {
return (
<div>
<Child onSubmit={this.onSubmit}/>
<h1>{this.state.result}</h1>
</div>
)
}
}
class Child extends React.Component {
state = {};
onChange = ({target: {name, value}}) => {
this.setState({
[name]: value
})
};
render() {
return (
<form onSubmit={() => this.props.onSubmit(event, this.state.name, this.state.country)}>
<input name="name" placeholder="name" onChange={this.onChange} value={this.state.name}/>
<input name="country" placeholder="country" onChange={this.onChange} value={this.state.country}/>
<input type="submit"/>
</form>
)
}
}

Adding a form to Gatsby JS, with an existing template that is export default

I am attempting to follow this tutorial to add a form to Gatsby JS. I understand it if my file wasn't setup differently. Firstly the tutorials component starts like this
export default class IndexPage extends React.Component {
Where I have this
export default ({ data }) => (
Then I am asked to place the following inside of it. I tried with both the render and return portion, and without.
state = {
firstName: "",
lastName: "",
}
handleInputChange = event => {
const target = event.target
const value = target.value
const name = target.name
this.setState({
[name]: value,
})
}
handleSubmit = event => {
event.preventDefault()
alert(`Welcome ${this.state.firstName} ${this.state.lastName}!`)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
First name
<input
type="text"
name="firstName"
value={this.state.firstName}
onChange={this.handleInputChange}
/>
</label>
<label>
Last name
<input
type="text"
name="lastName"
value={this.state.lastName}
onChange={this.handleInputChange}
/>
</label>
<button type="submit">Submit</button>
</form>
)
}
Here is all my code without the render and return portion
import React from 'react'
import { HelmetDatoCms } from 'gatsby-source-datocms'
import { graphql } from 'gatsby'
import Layout from "../components/layout"
export default ({ data }) => (
<Layout>
state = {
firstName: "",
lastName: "",
}
handleInputChange = event => {
const target = event.target
const value = target.value
const name = target.name
this.setState({
[name]: value,
})
}
handleSubmit = event => {
event.preventDefault()
alert(`Welcome ${this.state.firstName} ${this.state.lastName}!`)
}
<form onSubmit={this.handleSubmit}>
<label>
First name
<input
type="text"
name="firstName"
value={this.state.firstName}
onChange={this.handleInputChange}
/>
</label>
<label>
Last name
<input
type="text"
name="lastName"
value={this.state.lastName}
onChange={this.handleInputChange}
/>
</label>
<button type="submit">Submit</button>
</form>
<article className="sheet">
<HelmetDatoCms seo={data.datoCmsPricing.seoMetaTags} />
<section className="left-package-details">
<h1 className="sheet__title">{data.datoCmsPricing.title}</h1>
<p>
<span>${data.datoCmsPricing.priceAmount}</span> | <span>{data.datoCmsPricing.lengthOfSession}</span>
</p>
{data.datoCmsPricing.details.map(detailEntry => { return <li key={detailEntry.id}> {detailEntry.task}</li>})}
<p>
{data.datoCmsPricing.numberOfSessions}
</p>
book
<p>{data.datoCmsPricing.minimumMessage}</p>
</section>
<section className="right-package-details">
<img src={data.datoCmsPricing.coverImage.url} />
<div
className=""
dangerouslySetInnerHTML={{
__html: data.datoCmsPricing.descriptionNode.childMarkdownRemark.html,
}}
/>
</section>
</article>
</Layout>
)
export const query = graphql`
query WorkQuery($slug: String!) {
datoCmsPricing(slug: { eq: $slug }) {
seoMetaTags {
...GatsbyDatoCmsSeoMetaTags
}
title
priceAmount
details{
task
}
lengthOfSession
numberOfSessions
minimumMessage
descriptionNode {
childMarkdownRemark {
html
}
}
coverImage {
url
}
}
}
`
and the error I get is
There was a problem parsing "/mnt/c/Users/Anders/sites/jlfit-cms/src/templates/pricingDetails.js"; any GraphQL
fragments or queries in this file were not processed.
This may indicate a syntax error in the code, or it may be a file type
that Gatsby does not know how to parse.
File: /mnt/c/Users/Anders/sites/jlfit-cms/src/templates/pricingDetails.js
The problem you are facing is because you are trying to use state (and setState) on a functional component when the example uses a class.
Functional components don't have the same tools/syntax/APIs available to you as a class component (for better or worse) so you have to ensure you're using the correct approach for each case.
In the most recent versions of React you can have the equivalent of state and setState made available to you by using React hooks, more specifically the useState hook.
I've put together a quick working example of the code you pasted in your question converted to React hooks. You can find it on this sandbox.
I recommend you have a read over the initial parts of the React docs to ensure you're familiar with the foundational concepts or React, it will save a lot of headache in the future. 🙂

Setting the default value of an input field after data is retrieved causes the content to overlap and the "onChange" event not to be triggered

I have an "edit category" component in my React application.
The ID is passed through the URL.
When the component is mounted, the action "fetchCategory" is called, which updates the props on the component with the current category.
I have a form which I want to be pre-populated, which I'm currently doing using the defaultValue on the input.
However, this isn't reflected on the state and the label for the text field overlaps the input field.
Any help would be greatly appreciated. I'll leave snippets of my code below which could help with understanding what I'm trying to do.
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchCategory } from "../../store/actions/categoryActions";
class AddOrEditCategory extends Component {
componentDidMount() {
this.props.fetchCategory(this.props.match.params.id);
if (this.props.match.params.id) {
this.setState({
_id: this.props.match.params.id
});
}
}
handleSubmit = e => {
e.preventDefault();
console.log(this.state);
};
handleChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
render() {
const addingNew = this.props.match.params.id === undefined;
return (
<div className="container">
<h4>{addingNew ? "Add category" : "Edit category"}</h4>
<form onSubmit={this.handleSubmit}>
<div className="input-field">
<input
type="text"
id="name"
defaultValue={this.props.category.name}
onChange={this.handleChange}
/>
<label htmlFor="name">Category name</label>
</div>
<div className="input-field">
<input
type="text"
id="urlKey"
onChange={this.handleChange}
defaultValue={this.props.category.urlKey}
/>
<label htmlFor="urlKey">URL Key</label>
</div>
<button className="btn">{addingNew ? "Add" : "Save"}</button>
</form>
</div>
);
}
}
const mapStateToProps = state => {
return {
category: state.categoryReducer.category
};
};
export default connect(
mapStateToProps,
{ fetchCategory }
)(AddOrEditCategory);
EDIT: Included whole component as requested
You need to replace the 'defaultValue' attribute with 'value' in the inputs.
You are using a controlled vs uncontrolled component. You dont need to use defaultValue.
You can set the initial values on the promise success for fetchCategory
componentDidMount() {
this.props.fetchCategory(this.props.match.params.id).then(response => {
// Set the initial state here
}
}
OR in
componentWillReceiveProps(nextProps) {
// Compare current props with next props to see if there is a change
// in category data received from action fetchCategory and set the initial state
}
React docs
<form onSubmit={this.handleSubmit}>
<div className="input-field">
<input
type="text"
id="name"
onChange={this.handleChange}
value={this.state.name} //<---
/>
<label htmlFor="name">Category name</label>
</div>
<div className="input-field">
<input
type="text"
id="urlKey"
onChange={this.handleChange}
value={this.state.urlKey}
/>
<label htmlFor="urlKey">URL Key</label>
</div>
<button className="btn">{addingNew ? "Add" : "Save"}</button>
</form>

How can I get an input's value on a button click in a Stateless React Component?

I have the following functional component
const input = props => (
<div>
<input placeholder="Type a message..." />
<div onClick={props.send} className="icon">
<i className="fa fa-play" />
</div>
</div>
)
How could I possibly pass the value of the input to props.send()?
I found a solution for this exact scenario on React's official docs: https://reactjs.org/docs/refs-and-the-dom.html#refs-and-functional-components
This approach allows your component to remain stateless and also doesn't require you to update the parent component on every change.
Basically,
const input = props => {
let textInput = React.createRef();
function handleClick() {
console.log(textInput.current.value);
}
return (
<div>
<input ref={textInput} placeholder="Type a message..." />
<div onClick={handleClick} className="icon">
<i className="fa fa-play" />
</div>
</div>
)
}
Edit May 2021: Since this answer seems to be getting some attention, I have updated the answer to use a hooks based approach as well, since that is what I would use now (If using React 16.8 and above).
const input = props => {
const [textInput, setTextInput] = React.useState('');
const handleClick = () => {
console.log(textInput);
props.send(textInput);
}
const handleChange = (event) => {
setTextInput(event.target.value);
}
return (
<div>
<input onChange={handleChange} placeholder="Type a message..." />
<div onClick={handleClick} className="icon">
<i className="fa fa-play" />
</div>
</div>
)
}
There are many ways to do it since you're very much concerned about performance. Here is the implementation, your component will be rendered only when you click on send button which actually means state will be updated once and input value will be displayed in parent component.
const Input = props => {
return (
<div>
<input onChange={props.changeHandler} placeholder="Type a message..." />
<button onClick={props.send}>send</button>
</div>
);
};
class App extends Component {
state = {
inputValue: ""
};
inputValue = '';
send = () => {
this.setState({ inputValue: this.inputValue });
};
changeHandler = event => {
this.inputValue = event.target.value;
};
render() {
console.log("In render");
return (
<React.Fragment>
<Input changeHandler={this.changeHandler} send={this.send} />
<div> {this.state.inputValue}</div>
</React.Fragment>
);
}
}
Since you mentioned that you just started with React, I'd suggest that you work through the documentation (which offers nice explanation).
According to your comment, the usage of a functional component is not a requirement. Therefore I'd recommend to do it that way -->
Your CustomInput component:
import React from "react";
import PropTypes from "prop-types";
class CustomInput extends React.Component {
constructor() {
super();
this.textInput = React.createRef();
}
static propTypes = {
send: PropTypes.func
};
render() {
const { send } = this.props;
return (
<React.Fragment>
<input placeholder="Type a message..." ref={this.textInput} />
<div
onClick={() => send(this.textInput.current.value)}
className="icon"
>
CLICK ME
</div>
</React.Fragment>
);
}
}
export default CustomInput;
If you noticed, I've replaced the empty div with React.Fragment. In that case you can omit the unnecessary <div> wrappings (if those are not required) which will keep your DOM clean (Read more about it here.
Usage:
<CustomInput
send={(prop) => {
console.log(prop)
}}
/>
I just used a dummy function which will log the input value to the console..
You can check the working example (Make sure to trigger the console in the editor) here
Posting this answer, If incase someone is using an earlier release of React 16.3. We can achieve the same thing by using callback refs instead without having to maintain state or having onChange event handler:
const input = props => (
<div>
<input ref={props.myRef} placeholder="Type a message..." />
<div onClick={props.send} className="icon">
<i className="fa fa-play" />
</div>
</div>
)
Calling that Input Component
handleClick = () => console.log(this.inputRef.value);
<Input myRef={el => this.inputRef = el} send={this.handleClick} />

Resources