React/Redux need help removing items from a table - javascript

This is a super noob question, I'm learning redux and it's capabilities. However, I'm stuck while trying to remove an item from a list. This list has items added to it via a JSON object I imported and that is pushed into an array which I later loop through.
In one component I add the prices(mock data) through button clicks and I have a total (sum of the prices) working. This action also adds the name to the side list.
I've been searching but haven't found a good solution. I was hoping someone here can help me understand and/or point me in the right direction.
So far when I click the button it adds an empty button to the list instead than removing. Correct me if I'm wrong but this is the intended behavior for the slice function, to return an array?
Here is my initial state:
const initialState = [{
name: '',
total: '',
}]
My action type:
case ActionTypes.REMOVE_EXAMS:
return[
...state[0].name.slice(0, action.index),
...state[0].name.slice(action.index + 1)
]
case ActionTypes.ADD_PRICES:
return [
{
total: Number(state[0].total + action.price),
name: action.name
}
]
This is the component that renders a side list with all the items that have been added. My idea is to press the button and remove that item permanently from this side list
static propTypes = {
dataCarryName: PropTypes.string.isRequired,
removeExams: PropTypes.func.isRequired,
total: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.names = [];
}
render() {
{this.names.push(this.props.dataCarryName)}
{console.log(this.names)}
return (
<MuiThemeProvider>
<div className="side-exam-view">
<Paper zDepth={2}>
<h1> Carrito </h1>
{this.props.total}
{this.names.map((i, k) => {
return(
<ul key={k}
className="exam-list-names">
<li key={k}>
<button onClick={() => this.props.removeExams(( i.length, {i}))}> {i} - X</button>
</li>
<Divider />
</ul>
)
})}
</Paper>
</div>
</MuiThemeProvider>
);
}}
Update:
These are my action creators:
export const addPrices = (price, name) => {
return{
type: ActionTypes.ADD_PRICES,
price,
name
}}
export const removeExams = (index, name) => {
return {
type: ActionTypes.REMOVE_EXAMS,
index,
name
}}

Ok. I see at least a couple issues here and hopefully dealing with them will help you solve your problem.
First, I'm not sure that the way you're keeping your sum makes sense. I think what you really want to do is maintain a list of exams where each exam has a name and a price. You also want to be able to display the list of exam names and also the sum of the prices. So for starters I think your reducer should look more like this.
const initialState = [];
export default function reduce(state = initialState, action) {
switch (action.type) {
case ActionTypes.ADD_EXAM:
return state.concat({ name: action.name, price: action.price });
case ActionTypes.REMOVE_EXAM:
return state.filter(exam => exam.name === action.name)
default:
return state;
}
}
So ADD_EXAM adds a new entry to your array with a name and a price, and REMOVE_EXAM looks for the exam specified by action.name and filters it out.
You can easily display your sum like this:
const sum = exams.reduce((sum, exam) => sum + exam.price, 0);
And you can display your names like this:
const names = exams.map((exam, index) => <p key={index}>{exam.name}</p>);

Related

React - conditional/message

I have the following code and need to add two extra things to it but I'm stuck and not sure how to do it.
I need to add:
If there are no products in the category a NotFound component will show a message.
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
Ideally I'm looking for the simplest solution as I'm currently learning React. Thanks!
Main Component
import React from 'react';
import Item from './components/Item';
class App extends React.Component {
state = {
items: [
{
title: "The Spice Girls",
price: 10,
category: "Pop",
quantity: 1,
},
{
title: "Beethoven",
price: 5,
category: "Classical",
quantity: 1,
},
{
title: "Bob Marley",
price: 15,
category: "Reggae",
quantity: 1,
}
],
category: " ",
filtered: [],
}
handleChange = e => {
this.setState({category: e.target.value},()=>console.log(this.state.category));
}
handleClick = (event) => {
event.preventDefault()
var newList = this.state.items;
var filteredItems = newList.filter(item => item.category === this.state.category)
this.setState({filtered: filteredItems})
}
render () {
let show;
if(this.state.category !== " "){
show = this.state.filtered.map((item, i) => <Item key = {i} cd={item}/>)
}else{
show = this.state.items.map( (item,i) => <Item key = {i} cd={item}/>)
}
return (
<div>
<h1 className = "title">CD</h1>
<h2>Search music below:</h2>
<form>
Music style: <input onChange = {this.handleChange}></input>
<button onClick = {this.handleClick}>Search</button>
</form>
{show}
</div>
)
}
}
export default App;
Item Component
import React from 'react';
class Item extends React.Component {
render () {
return (
<div className = "items">
<div className = "item">
<h3>{this.props.cd.title}</h3>
<div className = "price">Price: {this.props.cd.price}€</div>
<div className = "quantity">Quantity: {this.props.cd.quantity}</div>
<div className = "category">Category: {this.props.cd.category}</div>
</div>
</div>
)
}
}
export default Item;
First of all some suggested changes before I answer your question
There are a few things which confused me much when analysing your code, so will share them with you, as if in the future you work on teams, it would be handy if other people can understand your code.
You have an on change event for the text box which is different to the event for the search button next to it. Users would expect it to be the same and so that's really confusing.
You have 2 lists of items essentially, a raw and unfiltered and you switch between which 2 to present on the screen. Sometimes you need to have a raw set and that's fine, but perhaps make sure that the only ones which are presented as such is just either the state.items or the state.filtered. I would probably expect the state.filtered
Make your search case insensitive, e.g. pop should match Pop
Answer to your question'ss
If there are no products in the category a NotFound component will show a message.
For this I would first modify your show logic to work on the same filtered list just change your event functions to manipulate the filtered list and untouch the items one.
add another condition perhaps for when there are no cases
if (this.state.filtered) {
show = this.state.filtered.map((item, i) => <Item key={i} cd={item} />);
} else {
show = <h1>NoneFound</h1>;
}
By typing 'all' in the input we should be able to see the entire list of products again from all the categories.
handleClick = event => {
event.preventDefault();
var { category } = this.state;
var newList = this.state.items;
var filteredItems;
if ([" ", "All"].some(t => t === category)) {
filteredItems = newList;
} else {
filteredItems = newList.filter(
item => item.category.toLowerCase() === this.state.category.toLowerCase()
);
}
this.setState({ filtered: filteredItems });
};
My souloution to this was to modify your onClick event to correctly manipulated the filtered list.
You can see my full soloution to this on my codesandbox here

Why do my items change order when I update input value in REACT?

I have simple shopping cart app made with REACT and REDUX.
I have some items that are displayed on screen with input filed where number of items is changed and based on that new price i calculated. But one thing is problematic.
Price is calculated correctly when I update number of items or when I add new but but when I update number that item is rendered last. So if I update number of first item it will just end as last item on the list
1
2
3
4
Let's say that I update item 1 then list will look like this
2
3
4
1
and this is really frustrating me. This is how I display items
{this.props.cart.map(item => {
return (
<div key={item.product.id}>
<Item item={item}/>
</div>
)
})}
I also tried with ...reverse().map but it does the same except it puts it on the top of page instead of bottom. I want them to stay where they are when they are updated
This is parent component of single item component. Both of them are class components, I also have this in parent component
const mapStateToProps = (state) => {
return {
cart: state.cart.cart
}
};
export default connect(mapStateToProps)(Cart);
Thank you for your time. If you need any other info just reach out and I will provide it.
UPDATE 1
This is how I update cart
handleChange = e => {
if(e.target.value <= 0) {
alert("Quantity must be grater than 0");
return;
}
if(e.target.value > this.props.item.product.amount) {
alert("Max number of products reached");
return;
}
if(this.state.quantity !== e.target.value) {
this.setState({
quantity: e.target.value,
// btnVisible: true
}, () => this.props.updateCartQuantity(this.props.item.product.id, this.state.quantity))
}
};
<input type="number" className="form-control" onChange={(e) => {this.handleChange(e)}} value={this.state.quantity}/>
This update is happening in child component
UPDATE 2
updateCartQuantity is a function
export const updateCartQuantity = (productId, quantity) => {
return {
type: 'UPDATE_CART_QUANTITY',
payload: {
productId,
quantity: quantity
}
}
};
it's data is handled here
case 'UPDATE_CART_QUANTITY':
let item = cart.find(item => item.product.id === action.payload.productId);
let newCart = cart.filter(item => item.product.id !== action.payload.productId);
item.quantity = action.payload.quantity;
newCart.push(item);
return {
...state,
cart: newCart
};
problem is probably in this case but I just can't see it
Okay. It is fixed thanks to #HMR comment on post. I just realised what I was doing.
Instead of cart.filter I should use this
let newCart = cart.map(c => c.id === action.payload.productId ? {...c, quantity: action.payload.quantity} : c);
and remove
newCart.push(item);
.map will take care of looping through elements so there is no need of pushing element to newCart.push(item)
Thank you for your comment. Hope this helps someone :)

setState long execution when managing large amount of records

I am trying to solve a problem that happens in react app. In one of the views (components) i have a management tools that operate on big data. Basically when view loads i have componentDidMount that triggers ajax fetch that downloads array populated by around 50.000 records. Each array row is an object that has 8-10 key-value pairs.
import React, { Component } from "react";
import { List } from "react-virtualized";
import Select from "react-select";
class Market extends Component {
state = {
sports: [], // ~ 100 items
settlements: [], // ~ 50k items
selected: {
sport: null,
settlement: null
}
};
componentDidMount() {
this.getSports();
this.getSettlements();
}
getSports = async () => {
let response = await Ajax.get(API.sports);
if (response === undefined) {
return false;
}
this.setState({ sports: response.data });
};
getSettlements = async () => {
let response = await Ajax.get(API.settlements);
if (response === undefined) {
return false;
}
this.setState({ settlements: response.data });
};
save = (key, option) => {
let selected = { ...this.state.selected };
selected[key] = option;
this.setState({ selected });
};
virtualizedMenu = props => {
const rows = props.children;
const rowRenderer = ({ key, index, isScrolling, isVisible, style }) => (
<div key={key} style={style}>
{rows[index]}
</div>
);
return (
<List
style={{ width: "100%" }}
width={300}
height={300}
rowHeight={30}
rowCount={rows.length || 1}
rowRenderer={rowRenderer}
/>
);
};
render() {
const MenuList = this.virtualizedMenu;
return (
<div>
<Select
value={this.state.selected.sport}
options={this.state.sports.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("sport", option)}
/>
<Select
components={{ MenuList }}
value={this.state.selected.settlement}
options={this.state.settlements.map(option => {
return {
value: option.id,
label: option.name
};
})}
onChange={option => this.save("settlement", option)}
/>
</div>
);
}
}
The problem i am experiencing is that after that big data is downloaded and saved to view state, even if i want to update value using select that has ~100 records it takes few seconds to do so. For example imagine that smallData is array of 100 items just { id: n, name: 'xyz' } and selectedFromSmallData is just single item from data array, selected with html select.
making a selection before big data loads takes few ms, but after data is loaded and saved to state it suddenly takes 2-4 seconds.
What would possibly help to solve that problem (unfortunately i cannot paginate that data, its not anything i have access to).
.map() creates a new array on every render. To avoid that you have three options:
store state.sports and state.settlements already prepared for Select
every time you change state.sports or state.settlements also change state.sportsOptions or state.settlementsOptions
use componentDidUpdate to update state.*Options:
The third option might be easier to implement. But it will trigger an additional rerender:
componentDidUpdate(prevProps, prevState) {
if (prevState.sports !== this.state.sports) {
this.setState(oldState => ({sportsOptions: oldState.sports.map(...)}));
}
...
}
Your onChange handlers are recreated every render and may trigger unnecessary rerendering of Select. Create two separate methods to avoid that:
saveSports = option => this.save("sport", option)
...
render() {
...
<Select onChange={this.saveSports}/>
...
}
You have similar problem with components={{ MenuList }}. Move this to the state or to the constructor so {MenuList} object is created only once. You should end up with something like this:
<Select
components={this.MenuList}
value={this.state.selected.settlement}
options={this.state.settlementsOptions}
onChange={this.saveSettlements}
/>
If this doesn't help consider using the default select and use a PureComponent to render its options. Or try to use custom PureComponents to render parts of the Select.
Also check React-select is slow when you have more than 1000 items
The size of the array shouldn't be a problem, because only the reference is stored in the state object, and react doesn't do any deep equality on state.
Maybe your render or componentDidUpdate iterates over this big array and that causes the problem.
Try to profile your app if this doesn't help.

onClick on "a" tag not working properly if it has some child elements in react

Below is the dynamic form which I created. It works fine. but Inside "a" tag if i add some child element to "a" tag, onClick event on "a" tag does not execute properly and pass proper name.
import React, { Component } from "react";
import "./stylesheet/style.css";
export default class Main extends Component {
state = {
skills: [""]
};
dynamicInputHandler = (e, index) => {
var targetName = e.target.name;
console.log(targetName);
var values = [...this.state[targetName]];
values[index] = e.target.value;
this.setState({
[targetName]: values
});
};
addHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
let values = [...this.state[targetName]];
values.push("");
this.setState({
[targetName]: values
});
};
removeHandler = (e, index) => {
e.preventDefault();
let targetName = e.target.name;
console.log(e.target.name);
let values = [...this.state[targetName]];
values.splice(index, 1);
this.setState({
[targetName]: values
});
};
render() {
return (
<div>
<form className="form">
{this.state.skills.map((value, index) => {
return (
<div className="input-row row">
<div className="dynamic-input">
<input
type="text"
placeholder="Enter skill"
name="skills"
onChange={e => {
this.dynamicInputHandler(e, index);
}}
value={this.state.skills[index]}
/>
</div>
<div>
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
</div>
</div>
);
})}
<button
name="skills"
onClick={e => {
this.addHandler(e);
}}
>
Add
</button>
</form>
{this.state.skills[0]}
</div>
);
}
}
i want to add Icon inside "a" tag , after adding icon tag, forms breaks and gives error "TypeError: Invalid attempt to spread non-iterable instance"
This works -
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
Remove
</a>
</span>
This does not (after adding icon inside a tag)
<span>
<a
name="skills"
className="close"
onClick={e => {
this.removeHandler(e, index);
}}
>
<i>Remove</i>
</a>
</span>
Inside removeHandler let targetName = e.target.parentElement.name;. When you wrap Remove in another tag, the event target is now the i tag, for which name is undefined.
I think this mixture of DOM manipulation (getting target of an event) is okay, but in React, where data is preferred over DOM values, you could work entirely on the component state and touch the DOM element values only once in the input where the skill value is displayed. Like this:
class Main extends Component {
state = {
skills: [],
lastSkillAdded: 0
};
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
render() {
return (
<form>
{this.state.skills.map((skill, index) => (
<div key={skill.id}>
<input
type="text"
value={skill.value}
onChange={e => this.updateSkillById(skill.id, e.target.value)}
/>
<button onClick={() => this.removeSkillById(skill.id)}>
Remove
</button>
</div>
))}
<button onClick={() => this.appendEmptySkill()}>Add</button>
</form>
);
}
}
Let's deconstruct what's going on there.
First, the lastSkillAdded. This is a controversial thing, and I guess someone will correct me if I'm wrong, but
when we iterate over an array to render, say, list items, it's recommended that we provide key prop that represents the item key,
and the value of key prop is not recommended to be the index of the array element.
So we introduce an artificial counter that only goes up and never goes down, thus never repeating. That's a minor improvement though.
Next, we have the skills property of component state. It is where all the values are going to be stored. We agree that each skill is represented by an object of two properties: id and value. The id property is for React to mark array items property and optimize reconciliation; the value is actual text value of a skill.
In addition, we have three methods to work on the list of skills that are represented by three component methods:
appendEmptySkill to add an empty skill to the end of the state.skills array,
updateSkillById to update the value of a skill knowing id and new value of it,
and removeSkillById, to just remove the skill by its id from state.skills.
Let's walk through each of them.
The first one is where we just append a new empty skill:
appendEmptySkill = () => {
this.setState(prevState => ({
...prevState,
skills: [...prevState.skills, { id: lastSkillAdded, value: "" }],
lastSkillAdded: prevState.lastSkillAdded + 1
}));
};
Because appending a new skill never depends on any input values, this method takes zero arguments. It updates the skill property of component state:
[...prevState.skills, { id: lastSkillAdded, value: '' }]
which returns a new array with always the same empty skill. There, we also assign the value to id property of a skill to our unique counter.
The next one is a bit more interesting:
updateSkillById = (id, value) => {
this.setState(prevState => ({
...prevState,
skills: skills.map(skill =>
skill.id !== id
? skill
: {
id,
value
}
)
}));
};
We agree that in order to change a skill, we need to know its id and the new value. So our method needs to receive these two values. We then map through the skills, find the one with the right id, and change its value. A pattern
(id, value) => array.map(item => item.id !== id ? item : ({ ...item, value }));
is, I believe, a pretty common one and is useful on many different occasions.
Note that when we update a skill, we don't bother incrementing our skill id counter. Because we're not adding one, it makes sense to just keep it at its original value.
And finally, removing a skill:
removeSkillById = id => {
this.setState(prevState => ({
...prevState,
skills: prevState.skills.filter(skill => skill.id !== id)
}));
};
Here, we only need to know the id of a skill to remove it from the list of skills. We filter our state.skills array and remove the one skill that matches the id. This is also a pretty common pattern.
Finally, in render, four things are happening:
existing skills are iterated over, and for each skill, input and "Remove" button are rendered,
input listens to change event and refers to EventTarget#value attribute in updateSkillById function call, because that's the only way to feed data from DOM back into React, and
the "Remove" buttons refers to skill id and calls removeSkillById,
and outside the loop, the "Add" button listens to click events and calls appendEmptySkill whenever it happens without any arguments, because we don't need any.
So as you can see, this way you're in total control of the render and state updates and never depend on DOM structure. My general advice is, if you find yourself in a situation when DOM structure dictates the way you manage component or app state, just think about a way to separate DOM from state management. So hope this answer helps you solve the issue you're having. Cheers!

Why React fails to render changes in array of JSX element when using .map index as a key?

Inside this codepen there is React application that renders list of ToDo's. It uses .map() index parameter as key values to render this list. And yes - I know that this is the source of this behaviour, so Please don't tell this in answers.
I've added few items there after initial load and then I see this:
Then I sort it by date and type something into first item
Then I click on Sort by Latest and get this:
Question: Why React fails to render changes in array of JSX element when using .map index as a key in this particular example?
P.S. Here is the code for convenience:
const ToDo = props => (
<tr>
<td>
<label>{props.id}</label>
</td>
<td>
<input />
</td>
<td>
<label>{props.createdAt.toTimeString()}</label>
</td>
</tr>
);
class ToDoList extends React.Component {
constructor() {
super();
const date = new Date();
const todoCounter = 1;
this.state = {
todoCounter: todoCounter,
list: [
{
id: todoCounter,
createdAt: date,
},
],
};
}
sortByEarliest() {
const sortedList = this.state.list.sort((a, b) => {
return a.createdAt - b.createdAt;
});
this.setState({
list: [...sortedList],
});
}
sortByLatest() {
const sortedList = this.state.list.sort((a, b) => {
return b.createdAt - a.createdAt;
});
this.setState({
list: [...sortedList],
});
}
addToEnd() {
const date = new Date();
const nextId = this.state.todoCounter + 1;
const newList = [
...this.state.list,
{id: nextId, createdAt: date},
];
this.setState({
list: newList,
todoCounter: nextId,
});
}
addToStart() {
const date = new Date();
const nextId = this.state.todoCounter + 1;
const newList = [
{id: nextId, createdAt: date},
...this.state.list,
];
this.setState({
list: newList,
todoCounter: nextId,
});
}
render() {
return (
<div>
<code>key=index</code>
<br />
<button onClick={this.addToStart.bind(this)}>
Add New to Start
</button>
<button onClick={this.addToEnd.bind(this)}>
Add New to End
</button>
<button onClick={this.sortByEarliest.bind(this)}>
Sort by Earliest
</button>
<button onClick={this.sortByLatest.bind(this)}>
Sort by Latest
</button>
<table>
<tr>
<th>ID</th>
<th />
<th>created at</th>
</tr>
{this.state.list.map((todo, index) => (
<ToDo key={index} {...todo} />
))}
</table>
</div>
);
}
}
ReactDOM.render(
<ToDoList />,
document.getElementById('root')
);
It seems that inputs are not changed at all here and as they are not controlled (so their attributes/text values didn't change), it makes sense as the root element is not changed in context of React reconciliation (it has still same key). Then React goes to text nodes that eventually are different and then refreshes (updates DOM) only those changed nodes.
So there are two variants to make this work:
Use meaningful key, not index from .map()
Add (in our specific case) attributes to <input />
In general first approach is better as the key property will invalidate the root element (in our case whole ToDo) and force it to render itself, saving us comparison of nested subtree.
Question: Why React fails to render changes in array of JSX element when using .map index as a key in this particular example?
Don't use the list index as a key. The point of the key is to identify your individual entries from one render to another. When you change the order of the array, react will not know that anything has changed, because the order of the indexes in the todo array is still 0, 1, 2, 3 ... Instead use the todo.id as key.
React hasn't failed to render the new order. It's doing exactly as expected. You are telling it the order is exactly the same as last render, but that the properties of the individual todo items have changed.
What you want to say is that it's the ordering that has changed. Then you have to pass a meaningful key. Then react will use the same dom elements, but change the order.
This means that you can do something like react-shuffle where a transition effect lets you see how the elements are reordered.
This is because you do not have stable ID's (your indexes change after the filter). See the docs here.
And you have another problem, because you are mutating your list when you sort it:
sortByEarliest() {
const sortedList = this.state.list.sort((a, b) => {
return a.createdAt - b.createdAt;
});
this.setState({
list: [...sortedList],
});
}
sortByLatest() {
const sortedList = this.state.list.sort((a, b) => {
return b.createdAt - a.createdAt;
});
this.setState({
list: [...sortedList],
});
}
The sort alter your array. So you could use this solution:
sortByEarliest() {
const { list } = this.state;
list.sort((a, b) => a.createdAt - b.createdAt);
this.setState({ list });
}
sortByLatest() {
const { list } = this.state;
list.sort((a, b) => b.createdAt - a.createdAt);
this.setState({ list });
}

Categories

Resources