React state one step behind (not duplicate!) - javascript

I know this may seem like a repeat and I have read the similar questions and answers. However I am still having an issue getting my parent component and its state to sync up with its child component. It is just one step behind, even in the console.
Parent Component App.js
changeItemQuantity = (id, newQuantity) => {
this.setState(prevState => ({
cartData: {
...prevState.cartData,
items: this.state.cartData.items.map(item => {
if (String(item.productData.productId) === String(id)) {
item.productQuantity = newQuantity
}
return item
})
}
}))
// console.log(id, newQuantity);
// console.log(this.state.cartData)
}
Child Component Cart.js
import React, { Component } from 'react';
class Cart extends Component {
constructor(props) {
super(props);
this.state = {
newQuantity: 1
}
}
handleQuantityChange = (e) => {
this.setState({
newQuantity: e.target.value
})
this.props.changeItemQuantity(e.target.id, this.state.newQuantity);
}
render() {
return (
<div className="cart-list">
{this.props.cartData.items.map(item => {
return (
<div className="cart-item" key={item.productData.productId}>
<div className={`cart-item-image ${item.productData.productImage}`}></div>
<div className="cart-item-data">
<div className="cart-item-name">{item.productData.productName}</div>
<div className="cart-item-s-q">( {item.productSize} ) x <input type="text" id={item.productData.productId} onChange={this.handleQuantityChange} className="cart-field change-q" placeholder={item.productQuantity}/></div>
<div className="cart-item-total">Total : ${(parseInt(item.productData.productPrice) * parseInt(item.productQuantity)).toFixed(2)}</div>
</div>
</div>
)
})}
</div>
)
}
}
export default Cart;
Basically what I am trying to accomplish is being able to be inside of the shopping cart page of my ecom site and adjust the quantity of each item (which is held inside of the parents state). I have that figured out, but when I adjust, the state inside the console and on the screen is not up to date. I have tried using a callback function like so
handleQuantityChange = (e) => {
this.setState({
newQuantity: e.target.value
}, this.props.changeItemQuantity(e.target.id, this.state.newQuantity);)
}
But that did nothing for me. I also tried using a callback inside of the Parent component like so...
changeItemQuantity = (id, newQuantity) => {
this.setState(prevState => ({
cartData: {
...prevState.cartData,
items: this.state.cartData.items.map(item => {
if (String(item.productData.productId) === String(id)) {
item.productQuantity = newQuantity
}
return item
})
}
}), console.log(this.state.cartData))
}
But again, nothing. What do you guys suppose I could do? Thanks in advance!

Related

How to fix my filtering for products in React

The task is to create a Fiter.jsx component that will filter Products.jsx (you can see it in the repository)
I have React Components :
Main.jsx
import React from "react";
import { Products } from "../Products";
import { Filter } from "../Filter";
import { Product } from "../Product";
class Main extends React.Component {
state = {
products: [],
filteredProducts: [],
status: "all",
};
onFilterStatusChange = (status) => {
this.setState({ status });
};
componentDidMount() {
fetch("./products.json")
.then((responce) => responce.json())
.then((data) => this.setState({ products: Object.values(data) }));
}
filterProducts() {
this.setState(({ status }) => ({
filteredProducts:
status === "all"
? this.state.products
: this.state.products.filter((n) => n.prod_status.includes(status)),
}));
}
componentDidUpdate(prevProps, prevState) {
if (this.state.status !== prevState.status) {
this.filterProducts();
}
}
render() {
// const { products } = this.state;
return (
<main className="container content">
<Filter
title="Status:"
values={["all", "recommended", "saleout", "bestseller", "new"]}
value={this.state.status}
onChange={this.onFilterStatusChange}
/>
<Products products={this.state.filteredProducts} />
</main>
);
}
}
export { Main };
Filter.jsx
const Filter = ({ title, values, value, onChange }) => (
<div>
<h3>{title}</h3>
{values.map((n) => (
<label>
<input
type="radio"
onChange={() => onChange(n)}
checked={value === n}
/>
{n}
</label>
))}
</div>
);
export { Filter };
At this stage, the console displays an error, and the site shows the following:
Question - help fix this...
Repo with all files
Before trying to create a filter, the site looked like this :
Hey as others have pointed that some products do not have a prod_status so change n.prod_status.includes(status) to n.prod_status?.includes(status) in filterProducts, which is in the Main component.
One more thing is that on the initial render it shows Nothing found but the status is set to all. It should show all the products available. To fix this
change the condition
this.state.status !== prevState.status in component.didUpdate
to
this.state.status !== prevState.status || this.state.products !== prevState.products.
Since on first render status value will not change ie. all but product changes from [] to [item...]. So check whether the products have changed or not and then run the filterProducts function.
Lastly, As the file products.json is in the public directory so in fetch call you should write the file path starting with a /.

In React Context, how can I use state variables in state functions?

I have a React Context which looks like this:
import React, { Component } from 'react'
const AlertsContext = React.createContext({
categoryList: [],
setCategoryList: () => {}
})
export class AlertsProvider extends Component {
state = {
categoryList: [],
setCategoryList: categoryString => (
this.categoryList.includes(categoryString)
? this.setState({ categoryList: this.categoryList.filter(value => value !== categoryString) })
: this.setState({ categoryList: this.categoryList.concat([categoryString]) })
)
}
render() {
const { children } = this.props
const {categoryList, setCategoryList } = this.state
return (
<AlertsContext.Provider value={{categoryList, setCategoryList}}>
{children}
</AlertsContext.Provider>
)
}
}
export const AlertsConsumer = AlertsContext.Consumer
So, categoryList is an array of strings, each representing a category. setCategoryList should take a string; if that string is already in the array, it removes it, and if it's not in the array it adds it.
In one of my components the user can select categories from a list of checkboxes. When a checkbox is clicked, the AlertsContext setCategoryList should be called with the value of the clicked box:
import React, { Component } from 'react'
import { AlertsConsumer } from '../../../context/alerts-context'
class AlertFilters extends Component {
constructor(props) {
super(props)
this.state = {
categories: props.categories
}
}
render() {
const { categories } = this.state
return (
<AlertsConsumer>
{({ categoryList, setCategoryList }) => (
<>
{
categories.map(category => (
return (
<div key={category.id}>
<Checkbox id={category.id} value={category.value} onChange={e => setCategoryList(e.target.value)} checked={categoryList.includes(category.value)} />
<label htmlFor={category.id}>{category.value}</label>
</div>
)
))
}
</>
)}
</AlertsConsumer>
)
}
}
export default AlertFilters
This compiles ok, but when I run it and click a checkbox I get the following error:
alerts-context.jsx:77 Uncaught TypeError: Cannot read property 'includes' of undefined
This is in the line:
this.categoryList.includes(categoryString)
in the Context Provider, suggesting that "this.categoryList" is undefined at this point.
I tried changing it to
this.state.categoryList.includes(categoryString)
but it said I had to use state destructuring, so I changed to:
setCategoryList: (categoryString) => {
const { categoryList } = this.state
categoryList.includes(categoryString)
? this.setState({ categoryList: categoryList.filter(value => value !== categoryString) })
: this.setState({ categoryList: categoryList.concat([categoryString]) })
}
which highlighted the ternary operator and gave the following lint error:
Expected an assignment or function call and instead saw an expression.
What am I doing wrong?
Use if/else syntax to update the state.
setCategoryList: categoryString => {
const { categoryList } = this.state;
if (categoryList.includes(categoryString)) {
this.setState({
categoryList: categoryList.filter(value => value !== categoryString)
});
} else {
this.setState({ categoryList: categoryList.concat([categoryString]) });
}
};

Any efficient way to render nested json list in React?

I am able to fetch REST API where I can get nested json output, and I want them to display in React component. Now I only can render them in the console which is not my goal actually. I am wondering if there is an efficient way to do this for rendering nested json list in React. can anyone give me a possible idea to make this work?
here is what I did:
import React, { Component } from "react";
class JsonItem extends Component {
render() {
return <li>
{ this.props.name }
{ this.props.children }
</li>
}
}
export default class List extends Component {
constructor(props){
super(props)
this.state = {
data: []
}
};
componentDidMount() {
fetch("/students")
.then(res => res.json())
.then(json => {
this.setState({
data: json
});
});
}
list(data) {
const children = (items) => {
if (items) {
return <ul>{ this.list(items) }</ul>
}
}
return data.map((node, index) => {
return <JsonItem key={ node.id } name={ node.name }>
{ children(node.items) }
</JsonItem>
});
}
render() {
return <ul>
{ this.list(this.props.data) }
</ul>
}
}
<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js"></script>
<div id="container">
<!-- This element's contents will be replaced with your component. -->
</div>
my current output:
in my above component, I could render nested list on the console like this:
[![enter image description here][1]][1]
desired output:
how can I properly render out nested json output on React? Any idea to make this happen? any thought? Thanks
As you knew .map() is the common solution for this. But you can make this much better like below.
export default class List extends Component {
constructor(props){
super(props)
this.state = {
data: [],
isLoaded: false, //initally the loading state is false.
}
};
componentDidMount() {
fetch("/students")
.then(res => res.json())
.then(json => {
//updating the loading state and data.
this.setState({data: json, isLoaded:true});
});
}
render() {
//Waiting ajax response or ajax not yet triggered.
if(!this.state.isLoaded){
return(<div>Loading...</div>);
}else{
//Rendering the data from state.
let studenDetails = this.state.data.map((student, i) => {
let uin = student.uin;
let studentInfo = Object.keys(student.studentInfo).map((label, i) => {
return (
<div key={i}>
<span>
<strong>{label}: </strong>{`${student.studentInfo[label]}`}
</span>
</div>
);
});
return (
<div key={i}>
<h3>{uin}</h3>
<p>{studentInfo}</p>
</div>
);
});
return (<div>{studenDetails}</div>);
}
}
}
Hope it will help you.
To render a list in react use the .map() function to build a list of jsx elements.
render() {
let myRenderedData = this.state.data.map((x, index) => {
return <p key={index}>{x.uin}</p>
})
return (<div>{myRenderedData}</div>)
}

Toggle Class in if else statement-React

I am trying to toggle a class in React (only in the else statement).
class Inf extends React.Component {
constructor() {
super();
this.state = {
pizzaData: data
}
}
renderList(info){
const list = this.state.pizzaData.map((entry, index) => {
if (entry.occupied==true){
return <li class="coloring" key={index}>Seat: {entry.seat}{entry.row}</li>;
}
else{
return <li class="colored" key={index}>Seat: {entry.seat}{entry.row}</li>;
}
});
return(
<ul>{list}</ul>
)
}
Now, looking over some of the documentation I was unsure how to do this. I know that there needs to be a "toggle" on the li and (I think) something like this below the this.state={:
pizzaData:data
},
handleClick function(
But I am not sure.
I created a simple example of how you can update your code, also with two components (similar to the idea by #THEtheChad), but without using context since according to react docs it is discouraged to use context directly if you want your app to be stable. If state and props management in app gets too complicated you can include redux (which internally also uses context), but for now I am not including redux since it be might over-complication in this simple case.
Here is PizzaList which has pizzas on its state. The component will render PizzaItem components and pass a callback down so that each PizzaItem can notify its parent (PizzaList) when it is clicked. PizzaList has the responsibility of toggling PizzaItem when it is clicked.
class PizzaList extends React.PureComponent {
state = {
pizzas: []
}
componentDidMount() {
// fetch data about pizzas via an API and perform this.setState
this.setState({ pizzas: [{ seat: 20, occupied: false }, { seat: 10, occupied: true }, { seat: 30, occupied: true }] });
}
handlePizzaItemClick = (pizzaInd) => {
this.setState((prevState) => {
// find clicked pizza and toggle its occupied property
const pizzas = prevState.pizzas.map((pizza, ind) => {
if (ind === pizzaInd)
return { ...pizza, ...{ occupied: !pizza.occupied } };
return pizza;
});
return { pizzas: pizzas };
});
}
render () {
return (
<ul>
{this.state.pizzas.map((pizza, index) =>
<PizzaItem
onClick={this.handlePizzaItemClick}
index={index}
pizza={pizza}
/>)}
</ul>
);
}
}
PizzaItem is a simple function component that doesn't have any state.
const PizzaItem = ({ index, pizza, onClick }) => {
const { seat, row, occupied } = pizza;
const pizzaClassName = occupied ? 'coloring' : 'colored';
return (
<li key={index}
className={pizzaClassName}
onClick={() => onClick(index)}>
Seat: {seat} {row}
</li>
);
}
Here is a working example on codesandbox.
I would update your code and split it into two components, a list component and an item component (in this case pizza?). The list component would provide a method for modifying the list using the context API. In my example, I have an updatePizza method that I pass down in the context.
Then, in the child component, you have a click handler that updates the occupied status of the pizza and tells the parent what the new status is using the context method.
This makes sure that the parent component always has the current state for all the pizzas and passes that down to the children. The parent component becomes the single source of truth here.
class List extends React.Component {
static childContextTypes = {
updatePizza: React.PropTypes.func
}
constructor({ pizzas }){
super()
this.state = { pizzas }
}
updatePizza = (idx, pizza) => {
this.setState( ({ pizzas }) => {
pizzas[idx] = pizza;
return { pizzas }
})
}
getChildContext() {
return { updatePizza: this.updatePizza }
}
render(){
return <ul>{this.state.pizzas.map((pizza, idx) => <Pizza key={ idx } { ...pizza }>)}<ul>
}
}
class Pizza extends React.Component {
static contextTypes = {
updatePizza: React.PropTypes.func
}
handleClick = () => {
this.state.occupied = !this.state.occupied;
this.context.updatePizza(this.state.key, this.state)
}
render() {
const { key, seat, row, occupied } = this.state;
const status = occupied ? 'coloring' : 'colored';
return <li key={ key } className={ status } onClick={ handleClick }> Seat: { seat } { row }</li>
}
}

componentWillReceiveProps this.props and nextProps always the same

I have a parent component ProductTest that holds state as the source of truth. The state is an array of variant items. Each item is simplified for this example and has a title.
When a Variant's title is updated, the child will call it's handler callback via the prop passed by the parent. The parent then updates state, and the child will then have a new title property.
What I am seeing is that the child's componentWillReceiveProps function will display the same value for this.props and nextProps. I would think that since the value is supplied by the parent, the two would be different.
I'm not sure what I'm doing incorrectly.
Here is the parent ProductTest component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
import axios from 'axios'
import Variant from './variants/Variant'
import { REACT_APP_API_URL, REACT_APP_SITE_KEY } from '../../../shared/vars'
import '../../../css/variants.css'
class ProductTest extends Component {
constructor(props) {
super(props)
this.state = {
variants: []
}
this.handleVariantTitleChange = this.handleVariantTitleChange.bind(this)
this.handleVariantValueChange = this.handleVariantValueChange.bind(this)
}
componentDidMount() {
this.loadData()
}
loadData() {
const apiEndpoint = 'products/some-product'
const AUTH_HEADER = { Authorization: REACT_APP_SITE_KEY }
return axios.get(REACT_APP_API_URL + apiEndpoint, { headers: AUTH_HEADER })
.then((response) => {
this.setState({ variants: response.data.product.variants })
})
.catch((err) => {
console.log("YA GOOFED BUD!", err)
})
}
handleVariantTitleChange(e, i) {
let { variants } = this.state
const { value } = e.target
variants = variants.map((item, index) => {
item.title = i === index ? value : item.title
return item
})
this.setState({ variants })
}
handleVariantValueChange(e, id) {
let { variants } = this.state
const { value } = e.target
variants = variants.map(variant => {
variant.items = variant.items.map(item => {
item.title = id === item.id ? value : item.title
return item
})
return variant
})
this.setState({ variants })
}
render() {
return (
<div>
{this.state.variants.map((variant, i) => {
return <Variant key={i}
variant={variant}
index={i}
changeVariantTitle={this.handleVariantTitleChange}
changeVariantValue={this.handleVariantValueChange}
/>
})}
</div>
)
}
}
export default ProductTest
Here is the child Variant component:
import React, { Component } from 'react'
class Variant extends Component {
componentWillReceiveProps(nextProps){
console.log(this.props, nextProps)
}
render() {
const { variant, index } = this.props
return (
<div className="variant-wrapper" data-new="new_value">
<div className="ajax-error" data-hbs-id="{{id}}"></div>
<div className="varient-item-titles">
<div className="variant-item-title">
Title
</div>
<div className="varient-attribute-titles">
<div className="variant-attribute">Value</div>
</div>
</div>
<div className="variant-item-content">
<div className="variant-label">
<input type="text" name="variant_label" value={variant.title} placeholder="Size, colour, etc."
onChange={e => this.props.changeVariantTitle(e, index)}
/>
</div>
</div>
</div>
)
}
}
export default Variant
Before
After
Console.log()
These two objects are this.props and nextProps. Notice the title property is not different, as I'd expect them to be.
SOLUTION!
The reason for all of those is that I was not correctly altering the parent's state immutably. I think I was just changing the value at the same memory address, and hence by the time it got to the componentWillReceiveProps it was already the new value. I think...
I found that the following will work:
handleVariantTitleChange(e, i){
const { value } = e.target
// Immutably clone specific variant item
let variant = Object.assign({}, this.state.data.variants[i])
// Set new title to item
variant.title = value
// Set the variant to the cloned item
let variants = this.state.data.variants
variants[i] = variant
this.setState({
data: {
...this.state.data,
variants
}
})
}

Categories

Resources