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

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.

Related

Cannot add to list of items in 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)}

React hooks - Prevent rerender if parent state that holds children props changed

I have an issue with my current project that uses react hooks.
What I'm trying to do is just to select my tasks by using (shift+click). Look like this:
Here is the code:
...
const [selectedTaskIds, setSelectedTaskIds] = useState<string[]>([])
const selectTask = useCallback(
(e: MouseEvent<HTMLDivElement>, taskId: string): void => {
e.stopPropagation()
const previousTaskId = selectedTaskIds[selectedTaskIds.length - 1]
if (previousTaskId && e.shiftKey) {
// handle shift+click
const previousIdx = tasks.findIndex((task) => task.id === previousTaskId)
const selectedIdx = tasks.findIndex((task) => task.id === taskId)
const rangeTasks =
previousIdx < selectedIdx
? tasks.slice(previousIdx, selectedIdx + 1)
: tasks.slice(selectedIdx, previousIdx + 1)
const rangeIds = rangeTasks.map((task) => task.id)
setSelectedTaskIds([...new Set([...selectedTaskIds, ...rangeIds])])
} else {
// if no key clicked, just select 1 task item
setSelectedTaskIds([taskId])
}
},
[selectedTaskIds, tasks] // <==== in here I notice that activeTaskIds is changed overtime that causes all of my <TaskItem> rerender
)
return (
{tasks.map((task) => (
<TaskItem
key={task.id}
taskId={task.id}
onClick={selectTask} // <=== selectTask will be different if user click on one of the task items
active={selectedTaskIds.includes(task.id)}
/>
))}
)
The problem is, to know which tasks should I select when the user uses shift+click, I need to know the currently selected task ids, so that I need to pass selectedTaskIds as a useCallback() deps.
That makes whenever the user selects the tasks or even just a click on one of the task items to select the task, it will re-render all of my <TaskItem> since the selectTask() function change due to useCallback's deps changed.
How can I solve this without rerender all of my <TaskItem>s? Thank you so much!
I tested your code on my machine and tested out a few scenarios. As far as I can tell, it looks natural for the component to re-render the all of the <TaskItem>s because any change in the selectedTaskIds state will guarantee everything inside the component that holds selectedTaskIds to render. To show you a concrete example,
<div className="App">
<TaskItems />
<div>hahaha</div>
<div>selectedTaskIds</div>
</div>
Let's say you have the above code. (I named your component that holds multiple <TaskItem/>s as <TaskItems/>) When onClick of <TaskItem/> triggers, only <TaskItems/> will re-render. The two other divs are not re-rendered. However, if you place the two divs inside the <TaskItems/> component, they will re-render:
// assuming this is inside <TaskItems/>
...
return (
<div>
{tasks.map((task) => (
<TaskItem
key={task.id}
taskId={task.id}
onClick={(e) => { selectTask2(e, task.id)}} // <=== selectTask will be different if user click on one of the task items
// active={selectedTaskIds.includes(task.id)}
active={true}
title={task.title}
/>
))}
<div>hahaha</div>
<div>selectedTaskIds</div>
</div>
);
above code will re-render the two divs.
I have tried to fulfill your request to get rid of the re-renders of the tasks that weren't changed, but it was really hard to do so. When I try to prevent re-rendering I usually use one of the two techniques:
create a child component and separate the code base to isolate groups of states. (since states are what triggers renders, you can
separate unrelated ones into different groups.)
useCallback/useMemo
Either techniques I failed to implement for your case, but there may be a way to apply the above techniques. I will follow the thread to see if anyone else gets a solution.

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.

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

React- onClick styling of currentTarget

I am trying to build a simple dynamically updated, interactive list that styles each <li></li> according to the css rules of a .clicked class, when you click on them.
The app is composed of two components, a parent and a child and the code in question is the following (taken from the child):
handleClick(e) {
document.getElementById(e.currentTarget.id).setAttribute("class","clicked");
}
render() {
let ar = this.props.sentences;
let pro = ar.map((x,i)=>{ return (<li id={i} key={i} className={i%2==0 ? "white" : "grey"}
onClick={this.handleClick}>{x}</li>); })
return (
<div>
<ul id="ul">{ pro }</ul>
</div>
What is happening here is basically that the parent is passing to the child a sentences prop (an array of sentences that will form the basis for the formation of a dynamic list).
The controversial part is me using DOM manipulation in the form of document.getElementById(e.currentTarget.id).setAttribute("class","two");
in order to change the class of the dynamically created html from jsx.
The code above works, however it does not feel as best practice. The whole advantage in using react is to use virtual dom and optimize the way the DOM is updated.
My questions are the following:
1) Am I right to feel this way? (that my solution is not best practice?)
2) (If so, ) How can I structure my code in order to use the virtual dom machinery react offers?
If you know this question to be a duplicate, please leave a comment and I ll remove it.
1) Am I right to feel this way? (that my solution is not best practice?)
It is correct to assume that this is not an ideal approach, manipulating the DOM via vanilla js in React has its place (Example Use Cases) but should not be done unless absolutely necessary. Also, it is not ideal to use the index from Array.prototype.map as the key on your components as if they change order it can cause confusion for React as the keys would map differently in that case.
2) (If so, ) How can I structure my code in order to use the virtual dom machinery react offers?
You should make use of the component state. If you want each clicked element to maintain the clicked class then make a piece of state that caches the elements that have already recieved the clicked class. if only the most recently clicked element gets the clicked class then simply cache an identifier to the appropriate element in the state. You could also use refs for this purpose though the overusage of them is somewhat discouraged by facebook.
Here is a quick snipped that will toggle the click class on each <li>
class Test extends Component {
constructor() {
super();
this.state = {
clicked: {}
};
}
render() {
let ar = this.props.sentences;
let pro = ar.map((x, i) => {
const color_class = i % 2 === 0 ? "white" : "grey";
const clicked_class = this.state.clicked[i] === true ? "clicked" : "";
let clicked = Object.assign({}, this.state.clicked); // Dont mutate state!!!
return (
<li
id={i}
key={i}
className={`${color_class} ${clicked_class}`}
onClick={e => {
if (clicked.hasOwnProperty(i)) {
delete clicked[i];
} else {
clicked[i] = true;
}
this.setState({ clicked });
}}
>
{x}
</li>
);
});
return (
<div>
<ul id="ul">
{pro}
</ul>
</div>
);
}
}

Resources