Cannot add to list of items in ReactJS - reactjs

Its been a while since I used ReactJS and I need to create a list of items that I can add to and remove. I've added an onClick event to my li to remove it. I also have a button to add new items, these seem to work but the state is not updating.
var new_items = [...Array(1)].map((val, i) => `No Items`);
<ul className="App-list">
{new_items.map((item, i) => (<li key={`item_${i}`} onItemClick={onItemClick(i)}>{ item }</li>))}
</ul>
the onClick function is here
function onItemClick(num) {
this.setState({
new_items: this.state.new_items.concat('new value')
})
}
I just need to either delete a line from the List or Add depending on status but even though it runs it does not update the state. Can someone give me either a batter way of updating a list of rows dynamically or tell me what I'm doing wrong.

You need to add a constructor as follows:
class MyClassName {
constructor(props) {
super(props);
this.state = {
new_items: [] // or null or any other initial value depending on your use case
}
this.onItemClick = this.onItemClick.bind(this);
}
function onItemClick(num){ ... }
}
Then while calling the onClick function you call it as follows:
onItemClick={this.onItemClick(i)}
Also, if you are using the generic onClick functionality, you would have to change onItemClick to onClick:
<li key={`item_${i}`} onClick={()=>this.onItemClick(i)}>
Since you are not using the event information from the click, you have to add an anonymous function that calls your desired onClick handler. Hence the ()=>this.onItemClick(i)

Since you are using Class Component you have to call the method with the context of class
onItemClick={this.onItemClick(i)}

Related

React.js re-render behavior changes when list contents change

This question requires some background to understand:
My app uses various lists of items with Boolean state that the user toggles by clicking.
I implemented this logic with two reusable elements:
A custom hook, useSelections(), which maintains an array of objects of the form { id, name, isSelected }, where isSelected is Boolean and the only mutable element. It returns the array as current state, and a dispatch function that takes an input of the form { id, newValue } and updates the isSelected member of the object with the given id
A function component Selector, which takes as props a single item and the dispatch from useSelections and returns a <li> element whose CSS class depends on isSelected.
Normally, they are used together in the following way, which works fine (i.e., internal state and the color of the list item are synchronized, and toggle when clicked):
function localComponent(props) {
const [items, dispatch] = useSelections(props.data);
return (
<ul>
{items.map(item => <Selector key={item.id} item={item} onChange={dispatch} />)}
</ul>
);
}
It works equally well when useSelections() is elevated to a parent component, and items and dispatch are passed as props.
The trouble started when the array became larger, and I decided to page it:
<ul>
{items.slice(start, end).map(item => <Selector key={item.id} item={item} onChange={dispatch} />)}
</ul>
(start and end are part of component state.)
When my component is first rendered, it works normally. However, once start and end are changed (to move to the next page), clicking an item changes internal state, but no longer triggers a re-render. In UX terms this means that after the first 'next' click, when I click on an item nothing appears to happen, but if I hit 'next' again, and then 'back', the item I just clicked on has now changed.
Any suggestions would be appreciated.
I solved the problem by removing the logic from the reducer function to ignore calls where there is no change:
const hasChanged = modifiedItem.isSelected !== newValue;
modifiedItem.isSelected = newValue;
return hasChanged ? { data } : state;
is now simply:
modifiedItem.isSelected = newValue;
return { data };
There's still something going on with React here that I do not understand, but my immediate problem is solved.

setState not .slicing() into state's array the way I'm intending

Background:
I'm developing an itinerary builder which is made up of rows, or component instances named EventContainers, that represent an activity on a user's given day.
The class, shown below, has an array in state.events that accepts EventContainers from the setState in the pushNewEventContainerToState function.
Of note, each EventContainer contains a button that is intended to give the user the ability to onClick an additional row/EventContainer by calling pushNewEventContainerToState.
The same button is also listed as its own component instance, named NewEventButton, and is displayed before any EventContainers.
Any EventContainer that is setStated to state.events is supposed to be placed in the index immediately after the EventContainer that calls setState, not at the beginning or end.
Method
I'm using .slice() in setState with the intention of doing just that => placing the newest EventContainer in the index immediately following the EventContainer that called setstate.
Problem
However, there are three issues I see:
a) Only the very first button, NewEventButton, will actually call setState. The buttons on the new EventContainer's won't do anything.
b) The EventContainers that pass through setState seem to be .pushed() to state.events, not .sliced(), but I don't need them at the end of the array.
c) When I check on Chrome devtools, I see that any EventContainer that's setStated to state.events is undefined.
What I've tried
I've tried placing two different kinds of props directly into the EventContainer that's inside pushNewEventContainerToState:
1. The first prop I tried didn't do anything -> onClick={() => this.pushNewEventContainerToState(index)
2. The second prop I tried was the same as the first except, instead of onClick, I named it pushNewEventContainerToState. This got the button on new EventContainers working but the EventContainers seemed to again be .pushed() instead of .sliced the way I need them to be. Chrome devtools will then show this is defined but I don't see a bound like I do for the first NewEventButton.
Thank you very much for taking a look.
class DayContainer extends React.Component {
constructor(props){
super(props);
this.state = {
events: [],
};
this.pushNewEventContainerToState = this.pushNewEventContainerToState.bind(this);
}
pushNewEventContainerToState(index) {
let newEvent = < EventContainer / > ;
this.setState(prevState => {
const updatedEvents = [...prevState.events.slice(0, index), newEvent, ...prevState.events.slice(index + 1)];
return {
events: updatedEvents
};
})
}
render(){
return (
<>
<div>
<ul>
{
this.state.events === null
? <EventContainer pushNewEventContainerToState={this.pushNewEventContainerToState} />
: <NewEventButton pushNewEventContainerToState={this.pushNewEventContainerToState} />
}
{this.state.events.map((item, index) => (
<li
key={item}
onClick={() => this.pushNewEventContainerToState(index)}
>{item}</li>
))}
</ul>
</div>
</>
)
}
}
Try fixing this part of your function first, looks like it doesn't behave as expected:
[...prevState.events.slice(0, index), newEvent, ...prevState.events.slice(index + 1)]
change to:
[...prevState.events.slice(0, index), newEvent, ...prevState.events.slice(index)]
.slice()'s second argument tells it to stop there, but doesn't include the index of the element. So if you have [1, 2, 3].slice(0,1) in example, you'll get only the first element -> [1].
Hope that fixes the issue.

Can React.createRef() be created dynamically from an array of state objects?

I have a list of styled radio buttons fixed inside the sidebar that, when clicked, scroll a corresponding component from the main area into view.
Both the radio buttons list and the components inside the main area are mapped from the state array called usedComponents that contains objects of component properties and ids. Let's say the array contains 10 objects and each object looks something like this:
{
id: 1,
group: "header",
componentType: "headerLogo",
componentName: "Header 01",
//...rest of the component properties
}
In order to achieve scroll into view on radio button click I had to create references to components inside the constructor. I manually created references for each component contained inside usedComponents state array and tied them to componentType like this:
this.headerLogo = React.createRef();
this.headerLogoNavigation = React.createRef();
//...etc.
I then passed the reference to each corresponding component and in my selectComponentHandler I set scrollIntoView to call the radio button id which corresponds to the main area component reference.
The selectComponentHandler looks like this:
selectComponentHandler = e => {
const value = Number(e.target.value);
const id = e.target.id; //returns componentType. For example "headerLogo"
this.setState(prevState => {
let selected = prevState.usedComponents
.filter(item => item.id === value)
.shift();
let unSelected = prevState.usedComponents
.filter(item => item.selected === true)
.shift();
if (unSelected) {
unSelected.selected = false;
}
selected.selected = true;
return { unSelected, selected };
});
this[id].current.scrollIntoView({ block: "start", behavior: "smooth" });
};
However, I'm using react-beautiful-dnd in order to be able to add new components by dragging them to the main area, and whenever I add the new component of the type that is already contained inside the usedComponents array, it has the same reference as the old component of the same type. This makes the scrollIntoView always scroll to the component of the same type that is first in line, no matter which one I select.
Is there a way to create references dynamically for all the components inside the usedComponents array, by perhaps mapping them, and update them whenever a new component is inserted. Maybe tie them to ids?
Edit:
I tried mapping references inside the constructor like this:
this.state.usedComponents.map(component => {
const id = component.id.toString();
return (this[id] = React.createRef());
});
It works for the components that are already inside the used Components array, however I still don't know how to update the references when the new object is inserted into the usedComponents array via drag and drop. Newly inserted components basically don't have references.
Like I was mentioning in the comments, I wouldn't go the route of dynamic refs. The scope / size of these refs could grow large and there is a much better way to handle this. Just use the DOM.
When rendering you can just put the components ID on your html element that you want to scroll to
<div id={component.id}> ... </div>
And then when you want to scroll to that element just query the DOM for that element and scroll to it
const elemToScrollTo = document.getElementById(component.id)
if (!!elemToScrollTo) {
elemToScrollTo.scrollIntoView()
}

Migrating away from componentWillReceiveProps

The componentWillReceiveProps is becoming deprecated, however, I am unclear as to how to migrate away from it. For example, a simplified version of my current looks something like this:
import Reorder, {reorder, reorderImmutale, reorderFromTo, reorderFromToImmutable} from 'react-reorder'
class ObjectsArea extends React.Component {
constructor(props) {
super(props);
this.state = {
items: this.props.objects ? this.props.objects.items : []
};
}
componentWillReceiveProps(nextProps){
//May have to do a deep compare between nextProps.items and current items?
if (nextProps.objects){
this.setState({items: this.nextProps.objects.items})
}
}
onReorder (event, previousIndex, nextIndex, fromId, toId) {
let new_items = reorder(this.state.items, previousIndex, nextIndex)
this.setState({
items: new_items
});
//call to parent function
}
render(){
orderable_items = <Reorder reorderId="objects" onReorder={this.onReorder.bind(this)}>
{
this.state.items.map(item => (
<div key={item.id}>
{item.text}
</div>
))
}
</Reorder>
return (
<div>{orderable_items}</div>
)
}
My requirements:
Sometimes there will be no objects property (there isn't one on initial load)
When there is an objects property a sortable/draggable list is created using the react-reorder component
When items in the list are dragged to be rearranged the onReorder function is called.
The onReorder function should do two things: update the list on the screen, call a parent function passed in from props.
Currently all of this will work with componentWillReceiveProps, however, what is the proper way to migrate away from componentWillReceiveProps based on the above requirements?
While Tolsee's answer is perfectly correct it is also worth mentioning that the react docs suggest removing derived state (state that is calculated based on props) altogether. There is a great article here that is a great read in my opinion.
Your example fits the Anti-pattern: Unconditionally copying props to state example perfectly.
Without knowing your environment I cannot recommend a solution certainly, but to me it looks like you will be able to use the Fully controlled component example.
In that case, you'd need to lift your state up, simply use objects.items to render your Reorder child, and during the onReorder event simply call a function that you received as a prop.
In your problem you can do.
static getDerivedStateFromProps(nextProps, prevState){
if (nextProps.objects){){
return {items: this.nextProps.objects.items};
}
else return null;
}
Please follow this post for better understanding

how to setstate on button click event in a table

I have master-detail table in react. If I select (or click a button) on a row in the master table, I want to set a state variable with the id of the selected row, which will be used to render the details table for that ID.
However, react does not allow me to use setState in the event function.
constructor(props){
super(props);
this.state = {
customer: {},
customerRequests: [],
interactions: [],
order: [],
error: '',
selectedCustomerRequest: ''
}
this.onSelectCustomerRequest = this.onSelectCustomerRequest.bind(this);
}
The event function is
onSelectCustomerRequest(id){
console.log(id)
// this.setState({selectedCustomerRequest: id});
}
The HTML is
<tr key={item._id} onClick={this.onSelectCustomerRequest(item._id)}>
<td><button onClick={this.onSelectCustomerRequest(item._id)} /></td>
<td>{moment(item.orderDate).format('DD/MM/YYYY hh:mm')}</td>
<td>{item.genericName}</td>
<td>{item.quantity}</td>
<td>{item.priority}</td>
<td>{interaction.orderStatus}</td>
<td>{moment(interaction.created).format('DD/MM/YYYY hh:mm')}</td>
<td>{interaction.createdBy}</td>
</tr>
if I enable the setState in onSelectCustomerRequest(id){} I get error. secondly I see that the onSelectCustomerRequest() gets executed for every row as I can see it console.log on my terminal.
I want to setState only when I click the button only.
I am sure I am doing a silly mistake somewhere, can you help? Please.
the problem here is that you call the function immediately by add parentheses () in the end of method.
To make it work, I would recommend create a closure within a method, so your case it would look like
onSelectCustomerRequest(id) {
return () => {
console.log(id)
}
}
So you basically by calling onSelectCustomerRequest in onClick return a function, which you can fire late.

Resources