Using usestate hook with array when getting data from redux store - reactjs

I'm getting my family from store like below from very top
const family:Family = useSelector((state:any) => state.family.family);
This is my family object
address: "No 48, Katukurunda"
enabled: true
id: 1
members: Array(2)
0: {id: "5", first_name: "Rohan", last_name: "Perera"}
1: {id: "4", first_name: "Sohani", last_name: "Perera"}
length: 2
__proto__: Array(0)
monthly_contribution: 50
name: "Mendis Family
as you can see clearly members have 2 items
So I have a UseState item to calculate row numbers like this
const [rowNumber, setRowNumber] = useState<number[]>([]);
I have a useEffect on top to setRowNumber like this
useEffect(() => {
if (family) {
family.members.forEach(() => {
setRowNumber([...rowNumber, rowNumber.length + 1]);
});
}
}, [family, setRowNumber]);
Just to check how many rows available when page loading I have added this code
useEffect(() => {
console.log(rowNumber);
}, [rowNumber]);
but above console log shows me this [1], an array with only one item.
What happened to the second member ?
I use rowNumber.map() to show available members of that Family. But when page loads it shows only one text box which is correct according to below useEffect
useEffect(() => {
console.log(rowNumber);
}, [rowNumber]);
What am I doing wrong here ?
Why rowNumber has only ONE item ?
family.members clearly has 2 items
I just realized only last value contains in rowNumber array

Could be related to the fact that setRowNumber is async. Have you tried to use a local array? Something like:
useEffect(() => {
if (family) {
let result = [];
family.members.forEach(() => {
result.push(result.length + 1);
});
setRowNumber(result);
}
}, [family, setRowNumber]);

Calling setState() in React is asynchronous, this means that setState does not update state immediately, so in the last iteration it will add 0 + 1 in your state.
try to do it like this:
useEffect(() => {
if (family) {
const rows = [];
family.members.forEach((value, i) => {
rows.push(i + 1);
});
setRowNumber(rows);
}
}, [family, setRowNumber]);

Content inside useEffect is wrong for storing array inside useState.
Use below code,
useEffect(() => {
if (family) {
family.members.map((item) => {
setRowNumber(()=> {
console.log(item); // verify result with this console log
return [...rowNumber, rowNumber.length + 1]
});
});
}
}, [family, setRowNumber]);

Related

In React object updates but does not pass updated object to next function

I am using React and have the following object:
const [records, setRecords] = useState(
[
{id: 1, correct: false},
{id: 2, correct: false},
{id: 3, correct: false},
{id: 4, correct: false},
{id: 5, correct: false}
]);
To update the object I have the following:
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
}
Here's the problem:
I want to run another function called isComplete after it but within the Handler function, that uses the changed records object, but it appears to use the original unchanged 'records' object (which console.log confirms).
e.g.
const onCorrectAnswerHandler = (id, correct) => {
setRecords(
records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
);
isComplete(records);
}
Console.log(records) confirms this. Why does it not use the updated records since the isComplete function runs after the update, and how can I get it to do this?
Try renaming the function as React sees no change in the object and likewise when you are using an array or object in a state. Try to copy them out by storing them in a new variable.
setRecords(
const newRecords = records.map((record) => {
if (record.id === id) {
return { id: id, correct: true };
} else {
return record;
}
})
//seting this now triggers an update
setRecords(newRecords);
);
Then as per react documentation it's better to listen to changes with lifecycle methods and not setting state immediately after they are changed because useState is asynchronous.
so use useEffect to listen to the changes to set is Complete
useEffect(() => {
isComplete(records)
}, [records])
I hope this helps you?
This is due to the fact that setState is not actually synchronous. The stateful value is not updated immediately when setState is called, but on the next render cycle. This is because React does some behind the scenes stuff to optimise re-renders.
There are multiple approaches to get around this, one such approach is this:
If you need to listen to state updates to run some logic you can use the useEffect hook.
useEffect(() => {
isComplete(records)
}, [records])
This hook is pretty straightforward. The first argument is a function. This function will run each time if one of the variables in the dependency array updates. In this case it will run each time records update.
You can modify above function onCorrectAnswerHandler to hold the updated records in temporary variable and use to update state and call isComplete func
const onCorrectAnswerHandler = (id, correct) => {
let _records = records.map((record) => {
if (record.id === id) {
return {
id: id,
correct: true
};
} else {
return record;
}
})
setRecords(_records);
isComplete(_records);
}
try this please.
const onCorrectAnswerHandler = (id) => {
records.forEach(r =>{
if(r.id == id) r.correct=true;
});
setRecords([...records]);
}

React State Updation Issue

My component's state is as below:
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {},
});
teamMembersOptions are being mapped from the redux state teamMembersList as below:
const teamMembersList = useSelector(state => state.get_all_team_members.team)
useEffect(() => {
if (teamMembersList)
mapTeamMembers();
}, [teamMembersList])
const mapTeamMembers = () => {
const teamMembers = [];
teamMembersList.map(member => {
const memberObject = {
'value': member.id,
'label': member.first_name.charAt(0).toUpperCase() + member.first_name.slice(1) + ' ' + member.last_name.charAt(0).toUpperCase() + member.last_name.slice(1)
}
if (member.is_leader == 1) {
memberObject.label = memberObject.label + ' (owner)'
setState({
...state,
selectedTeamMember: memberObject
})
}
teamMembers.push(memberObject)
})
setState({
...state,
teamMembersOptions: teamMembers
})
}
The state variables of selectedTeamMember and teamMemberOptions are not updating, it keeps consoling empty state. Whenever I console the local array of teamMembers inside mapTeamMembers function, it logs all the values successfully teamMembersList from Redux
also logs successfully that means teamMembersList and teamMembers are not empty. But the state is not updating. Why the setState statement inside mapTeamMembers function is not updating the state?
There are a number of things going on here and lot of them cause renders to trigger more renders which is why you are getting unexpected output.
I have add useMemo() and useCallback() around the data and calculation method respectively, and added their return values to the dependency array for useEffect(). This is to avoid the useEffect dependencies change on every render.
Calling setState() within the .map() function doesn't feel like the right choice either as each time it is called a render might occur, even though you are halfway through the mapping operation. Instead I suggest, and opted for, using .reduce() on the array and returning that result which can then be used to update the state within the useEffect hook.
Have a look at the working code below and a sample output given the defined input from teamMembersList. Note: this doesn't use Redux in the example given that it more setup to prove the concept.
import { useCallback, useEffect, useMemo, useState } from "react";
export default function App() {
const [state, setState] = useState({
teamMembersOptions: [],
selectedTeamMember: {}
});
const teamMembersList = useMemo(
() => [
{ id: 1, first_name: "John", last_name: "Smith", is_leader: 0 },
{ id: 2, first_name: "Maggie", last_name: "Simpson", is_leader: 1 }
],
[]
);
const mapTeamMembers = useCallback(
() =>
teamMembersList.reduce(
(acc, member) => {
const memberObject = {
value: member.id,
label:
member.first_name.charAt(0).toUpperCase() +
member.first_name.slice(1) +
" " +
member.last_name.charAt(0).toUpperCase() +
member.last_name.slice(1)
};
if (member.is_leader === 1) {
memberObject.label = memberObject.label + " (owner)";
acc.leader = memberObject;
}
acc.teamMembers.push(memberObject);
return acc;
},
{
teamMembers: [],
leader: ""
}
),
[teamMembersList]
);
useEffect(() => {
if (teamMembersList) {
const members = mapTeamMembers();
setState({
selectedTeamMember: members.leader,
teamMembersOptions: members.teamMembers
});
}
}, [teamMembersList, mapTeamMembers, setState]);
return (
<div>
<pre>
<code>{JSON.stringify(state, null, 4)}</code>
</pre>
</div>
);
}
The above will render out:
{
"selectedTeamMember": {
"value": 2,
"label": "Maggie Simpson (owner)"
},
"teamMembersOptions": [
{
"value": 1,
"label": "John Smith"
},
{
"value": 2,
"label": "Maggie Simpson (owner)"
}
]
}
I'd consider splitting the state object into individual state items but that's really up to you and how you want to handle the data.

How can I change the array key value before passing to the state in react?

Question, I have this state coming from the backend It's a array of messages that will be store in state using useState. This state will be pass on in the child component. The problem is I want to change value of a specific key before storing it into the state.
Sample
Messages Array sample data
const messages = [
{
value: 'sample value one',
status: false,
},
{
value: 'sample value two',
status: false,
},
];
UseSelector
const messageGetById = useSelector((state) => state.messageGetById);
const { message } = messageGetById;
UseEffect
useEffect(() => {
if (message) {
setCurrentMessage(message);
}
}, [message]);
The output that I want is before passing the message into setCurrentMessage, all the value of status will be change to true.
Thanks!
You can use map method to map thought the array and change the status to true.
useEffect(() => {
if (message) {
const newMessages = messages?.map((mess) => {
return {...mess, status: true}})
setCurrentMessage(newMessages);
}}, [message]);
Set the mapped state with useEffect
useEffect(() => {
const data = [...message];
if (data.length > 0) {
data.map((ele) => ({
value: "YOUR CHANGED VALUE",
status: ele.status,
}));
setCurrentMessage(data);
}
}, [message]);

Using reselect with a date field on an object-based reducer

I have a simple task list app. One of the screens is a "Today & Overdue" list.
The tasks reducer looks like:
{
"data": {
123: {
"id": 123,
"summary": "blah blah",
"dueDate": "2020-03-12",
"completed": true
},
456: {
"id": 456,
"summary": "some other task",
"dueDate": "2020-03-12",
"completed": false
}
},
"byId": [123, 456]
}
My list reducer looks like:
{
"data": {
919: {
"id": 919,
"name": "Today & Overdue"
},
818: {
"id": 818,
"summary": "My Cool List"
}
},
"byId": [919, 818]
}
On the "Today & Overdue" list, I need to fetch all tasks where the dueDate is today or older. I tried using reselect to optimize for the performance of the list screen, via:
# Get end of day today
const now = moment();
const endOfDay = Date.parse(now.endOf("day").utc(true).utcOffset(0).format());
const getTasksTodayOrOlder = (state) => Object.values(state.tasks.data).filter(task => Date.parse(task.dueDate) <= endOfDay);
But it appears that any time a field in the tasks data changes (i.e. completed or summary), the getTasksTodayOrOlder regenerates the selector.
Is the only way to do this to keep a cache on the tasks reducer; something like byDueDate to keep track of an array of arrays of due dates.
{
"data": ...,
"byId": ...,
"byDueDate": {
"2020-03-19": [112,123,141, ...],
"2020-03-20": [922, 939, ...],
}
}
The date cache seems like a lot of overhead and could get out of sync.
What is the recommended way to handle a reselect that will:
Will filter to tasks due today or older
Tasks that are not complete
If the output of a selector is a calculated array that uses Object.keys, Object.values or Array.prototype.filter then you can memoize it in the following way:
const { createSelector, defaultMemoize } = Reselect;
const state = [
{ val: 1 },
{ val: 2 },
{ val: 3 },
{ val: 4 },
{ val: 5 },
{ val: 6 },
{ val: 7 },
];
//pass an array to memArray like [a,b], as long as a and b are the same
// you will get the same array back even if the arrays themselves
// are not the same like when you use filter, Object.values or Object.keys
const memArray = (() => {
const mem = defaultMemoize((...args) => args);
//pass the array that this function receives to a memoized function
// as separate arguments so if [a,b] is received it'll call
// memoized(a,b)
return arr => mem(...arr);
})();//this is called an IIFE
const selectHigher = createSelector(
state => state,
(_, min) => min,
(data, min) =>
memArray(
Object.values(data).filter(({ val }) => val > min)
)
);
const one = selectHigher(state, 5);
const twoState = [...state, { val: 0 }];
const two = selectHigher(twoState, 5);
console.log('two is:',two);
console.log('one and two are equal', one === two);
const threeState = [...state, { val: 8 }];
const three = selectHigher(threeState, 5);
console.log('three is:',three);
console.log('three and two are equal', three === two);
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
What does it mean "regenerates the selector"? Where you using this selector? If you are using it in react functional component via hook (useSelector) for example, then:
// selectors
const now = moment();
const endOfDay = Date.parse(now.endOf("day").utc(true).utcOffset(0).format());
const getTasks = (state) => state.tasks.data;
const getTasksTodayOrOlder = createSelector(getTasks, tasks =>
Object.values(tasks)
.filter(task => Date.parse(task.dueDate) <= endOfDay)
.map(({ id, dueDate }) => {id, dueDate});
// component
import { shallowEqual, useSelector } from 'react-redux';
.
.
.
const tasksTodayOrOlderList = useSelector(getTasksTodayOrOlder, shallowEqual);
Anytime something in state.tasks.data changes, getTasksTodayOrOlder will be recalculating, but you will not get re render if previous state of tasksTodayOrOlderList is shallow equally to current output of getTasksTodayOrOlder selector (all values inside objects are equal), because we passed second argument shallowEqual to our useSelector function. I used map to remove "tracking" from unnecessary properties from our data object.
And we need to split our selector into two, because we only need to recalculate if our state.tasks.data changes, not when any part of our state changes.
Also, i think your should use endOfDay as arg value to selector, because it's dynamic.

Adding to an array within an array state in ReactJS

I'm trying to add functionality for adding to a state, more specifically "list", in the following state:
shoeList : [
{name: 'Nike',
list : [
{type: 'boots', revenue: '1000000', gender: 'mens', price: '49.99', id: 3},
{type: 'shoes', revenue: '13280100', gender: 'womens', price: '99.99', id: 2}
]
}
],
Right now I have a component that displays a form for the user to enter new values for type revenue gender and price.
Here is the code for the component(not including the forms and text input html):
state = {
}
//when changes occur in text input fields
handleChange = (e) => {
this.setState({
[e.target.id]: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault();
this.props.addShoe(this.state);
And in the root component i have the addShoe function:
addShoe = (shoe) => {
shoe.list.id = Math.random();
//returns a new array so no alteration of original array
let shoeList = [...this.state.shoeList, shoe];
this.setState({
shoeList: shoeList
})
}
Trying this code gives me an error saying shoe.list.id is undefined? Also I think I'm missing something to add in the component file specifically in the state. Also is there any way to directly access list like this.state.shoeList.list? I'm not sure if i have to add list to shoeList. Any help would be great thanks
In your example, if the intention is to add an item to specifically the Nike list:
addShoe = (shoe) => {
this.setState({
// return an altered copy of the array via map
shoeList: this.state.shoeList.map(brandItem => {
if (brandItem.name === 'Nike') {
return {
// use spread syntax for other object properties to persist
...brandItem,
list: [
// use spread syntax to keep the original items in the list
...brandItem.list,
{
// assuming shoe is an object without an id property
...shoe,
id: Math.random()
}
]
}
} else {
return brandItem;
}
})
}

Resources