React re-render problem with same data but different object of API - reactjs

I am hitting a backend API which contains array of objects. I am storing the API data in a state called reportsTabData. This state contains array of objects and one object who's key is reports contains array of objects called options and each options contains array of objects called fields. I am setting the fields array of objects in my state called optionsTabData and I am creating a UI based on optionsTabData.
Example:
[
..some objects,
{
key: "report",
default: "incoming"
options: [
{
type: "incoming",
fields: [
{
colspan: "1"
key: "CLAppSchedulerRepDependentSelectors"
label: "Extension(s) Selection"
type: "CLAppSchedulerRepDependentSelectors"
}
]
},
{
type: "outgoing",
fields: [
{
colspan: "1"
key: "CLAppSchedulerRepDependentSelectors"
label: "Extension(s) Selection"
type: "CLAppSchedulerRepDependentSelectors"
}
]
}
]
}
]
The code works fine and UI is generated, but as you see, there are some reports who's fields contain same objects. The problem is the state update is not re-rendering for such use-case else it works fine.
Below is the code:
const [loader, setLoader] = useState(false);
const [optionsTabData, setOptionsTabData] = useState([]);
useEffect(() => {
callForApi()
},[])
const callForApi = async () => {
setLoader(true);
let response = await getRequest("api-url");
if (response.status == 200 && response.status <= 300) {
let adapter = response.data.schema;
setAdapterData(adapter);
setReportsTabData(adapter.report.fields);
setDistributionTabData(adapter.distribution.fields);
let data = [];
for (const ele of adapter.report.fields) {
if (ele.key === "report") {
for (const item of ele.options) {
if (item.type === ele.default) {
data = [...item.fields];
break;
}
}
break;
}
}
setOptionsTabData(data);
} else {
toastErrorMessagePopUp(response.data.detail);
}
setLoader(false);
};
const handleDropdownChange = (key, value) => {
if (key === "report") {
const schema = [...reportsTabData];
let report = schema.find((ele) => ele.key === "report");
let newOptions = []
for (const ele of report.options) {
if (ele.type === value) {
newOptions=[...ele.fields];
break;
}
}
setOptionsTabData([...newOptions])
}
}
How do I solve this? Apparently, doing something like await setOptionsTabData([]) before setOptionsTabData([...newOptions]) solves it but I think using await is a bad idea and also the value reflecting after a click in dropdown is sluggish and slow.

You can "force" the re-render by using a combination of JSON.stringify and JSON.parse
setOptionsTabData(JSON.parse(JSON.stringify(newOptions)))

It looks like it should be working. You are passing new array to setOptionsTabData which should trigger a rerender.
Using await is not a solution - functions returned by useState are synchronous and that should not make a difference. What's actually happening is you are resetting your optionsData state to an empty array and then setting it to the desired data.
Is there more code you can show? Maybe you have a useEffect somewhere that updates your UI with unfulfilled dependencies?

Related

after sort, the ui not render with new data

im trying to list out the list after sort by status. This is my code, it can be sort in order.But the problem is, at the ui screen, it still previous value which it not render after do sorting.
const [listData, setListData] = useState(null)
let statusOrder={
'Active':1,
'In progress'
}
let list=[
{
sample: '1',
data:[
{
name:'name1',
status:'In Progress'
},
{
name:'name2',
status:'Active'
}
]
},
{
sample:'2',
data:[
{
name:'name1',
status:'In Progress'
},
{
name:'name2',
status:'Active'
}
]
}
]
const function = () => {
setListData(list)
for(let i=0; i<listData.length; i++){
listData[i].data.sort(a,b) => statusOrder[a.status] - statusOrder[b.status]
if((index+1) == listData.length)
setData(listData)
}
}
useEffect(() => {
sort()
},[])
I already try this approach, but still not render.
setListData([].concat(listData[i].data).sort((a, b) => statusOrder[a.status] - statusOrder[b.status]))
Like mentioned in the comments you're not really setting a new state since the "new" state is the same object as the old one.
Your function is called function which is a reserved keyword in the JS. So I assume you mean sort like you're using in your useEffect.
To handle the sort or your list you can try this
const sort = () => {
// give a callback to the setState which receives the prevState
setListData((prevListData) => {
// map over the prevState which returns a new array
const newListData = prevListData.map((item) => {
// return a new object with the item and sorted data
return {
...item,
data: item.data.sort(
(a, b) => statusOrder[a.status] - statusOrder[b.status]
),
};
});
return newListData;
});
};

updating object inside array inside object using prevState and the useState hook

I'd like to remove a nested object based on the id is equal to a passed prop. At the moment, the entire object is replaced. I'm missing something, when trying to update the state using useState probably with the way I'm looping my object?
UPDATE: The question was closed in response to available answers for updating nested objects. This question involves arrays which I believe are part of the issue at hand. Please note the difference in nature in this question with the forEach. Perhaps a return statement is required, or a different approach to the filtering on id..
my initial object looks like this:
{
"some_id1": [
{
"id": 93979,
// MORE STUFF
},
{
"id": 93978,
// MORE STUFF
}
],
"some_id2": [
{
"id": 93961,
// MORE STUFF
},
{
"id": 93960,
// MORE STUFF
}
]
}
and I go through each item as such:
for (const key in items) {
if (Object.hasOwnProperty.call(items, key)) {
const element = items[key];
element.forEach(x => {
if (x.id === singleItem.id) {
setItems(prevState => ({
...prevState,
[key]: {
...prevState[key],
[x.id]: undefined
}
}))
}
})
}
There are 3 problems in your code:
You are setting the value of key to an object while the items is expected to have an array to ids.
// current
[key]: {
...prevState[key],
[x.id]: undefined
}
// expected
[key]: prevState[key].filter(item => item.id === matchingId)
If you intend to remove an object from an array based on some condition, you should be using filter as pointed out in Owen's answer because what you are doing is something else:
const a = { xyz: 123, xyz: undefined };
console.log(a); // { xyz: undefined} - it did not remove the key
To make your code more readable, it is expected to manipulate the entire object items and then, set it to the state once using setItems - in contrast to calling setItems multiple times inside a loop and based on some condition.
This makes your code more predictable and leads to fewer re-renders.
Also, the solution to your problem:
// Define this somewhere
const INITIAL_STATE = {
"some_id1": [
{
"id": 93979
},
{
"id": 93978
}
],
"some_id2": [
{
"id": 93961
},
{
"id": 93960
}
]
};
// State initialization
const [items, setItems] = React.useState(INITIAL_STATE);
// Handler to remove the nested object with matching `id`
const removeByNestedId = (id, items) => {
const keys = Object.keys(items);
const updatedItems = keys.reduce((result, key) => {
const values = items[key];
// Since, we want to remove the object with matching id, we filter out the ones for which the id did not match. This way, the new values will not include the object with id as `id` argument passed.
result[key] = values.filter(nestedItem => nestedItem.id !== id)
return result;
}, {});
setItems(updatedItems);
}
// Usage
removeByNestedId(93961, items);
Probably a simple reduce function would work, Loop over the entries and return back an object
const data = {"some_id1": [{"id": 93979},{"id": 93978}],"some_id2": [{"id": 93961},{"id": 93960}]}
const remove = ({id, data}) => {
return Object.entries(data).reduce((prev, [nextKey, nextValue]) => {
return {...prev, [nextKey]: nextValue.filter(({id: itemId}) => id !== itemId)}
}, {})
}
console.log(remove({id: 93961, data}))
your way solution
for (const key in items) {
if (Object.hasOwnProperty.call(items, key)) {
const element = items[key];
element.forEach(x => {
if (x.id === singleItem.id) {
setItems(prevState => ({
...prevState,
//filter will remove the x item
[key]: element.filter(i => i.id !== x.id),
}))
}
})
}
}
short solution.
for(const k in items) items[k] = items[k].filter(x => x.id !== singleItemId);
const items = {
"some_id1": [
{
"id": 93979,
},
{
"id": 93978,
}
],
"some_id2": [
{
"id": 93961,
},
{
"id": 93960,
}
]
}
const singleItemId = 93979;
for (const k in items) items[k] = items[k].filter(x => x.id !== singleItemId);
console.log(items);
//setItems(items)
You could try using the functional update.
const [data, setData] = [{id:1},{id:2},{id:3}...]
Once you know the id which you need to remove.
setData(d=>d.filter(item=>item.id !== id));

React.js: Loop through a state array to create multiple JSON request

I'm trying to create this type of JSON request for multiple deletion of records
[
{
"id" : "618D7054-339E-4B58-ADD2-45C0A4D2B915",
"maker": "TESTMAKER",
"status": "DELETED"
},
{
"id" : "E698C439-EDB8-4652-9365-F64B3000B97E",
"maker": "TESTMAKER",
"status": "DELETED"
}
]
The screen is composed of a datatable with checkboxes
I can now get the id of each record when checked with this code:
const selectRowBrstn = {
mode: 'checkbox',
clickToSelect: true,
clickToEdit: false,
onSelect: (row, isSelect, rowIndex, e) => {
console.log("selectRowBrstn - onSelect row >>> " + row.id)
setCheckedBrstnRecords([...checkedBrstnRecords, row.id]);
},
onSelectAll: (isSelect, rows, e) => {
console.log("selectRowBrstn - onSelectAll rowIndex >>> " + rows)
}
};
With this, I'm putting the values in the state (array):
const [checkedBrstnRecords, setCheckedBrstnRecords] = useState([]);
Now, I have an array of ids in the state
The challenge now is for me to create multiple axios POST requests with different ids coming from the state
I have little idea on how to manipulate objects in the state
May I know what's the best approach for this?
Btw, here is the axios POST request:
deleteBrstnMaintenance(checkedBrstnRecords){
return axios.post(`${API_URL_SAVE_BANK_MAINTENANCE}`,
[
{
}
])
}
TIA
You could just iterate over your state. It depends at which time you want to fire the requests and how many at a specific time.
What you are probably looking for is:
useEffect(() => { // useEffect will be called if the variable in the provided array (in this case checkedBrstnRecords changes
Object.keys(checkedBrstnRecords).forEach(el => { // iterate over the state
const timer = setTimeout(() => {
const currElem = checkedBrstnRecords[el]; // here is your element
let res = await axios.post(API_URL_SAVE_BANK_MAINTENANCE);
}, 70);
return () => clearTimeout(timer); // clear timer
})
}, [checkedBrstnRecords]);

React state comparison with objects and nested arrays

I am trying to compare state against a previous version of the same object (useRef) to detect for changes.
The object that loads into state with useEffect looks like this:
{
active: true,
design: [
{
existing: '55Xgm53McFg1Bzr3qZha',
id: '38fca',
options: ['John Smith', 'Jane Doe'],
required: true,
title: 'Select your name',
type: 'Selection List'
},
{
existing: '55Xgm53McFg1Bzr3qZha',
id: '38fca',
options: ['John Smith', 'Jane Doe'],
required: true,
title: 'Select your name',
type: 'Selection List'
},
],
icon: '🚜',
id: '92yIIZxdFYoWk3FMM0vI',
projects: false,
status: true,
title: 'Prestarts'
}
This is how I load in my app object and define a comparitor state (previousApp) to compare it and detect changes (with lodash) this all works great until I change a value in the design array.
const [app, setApp] = useState(null)
const [edited, setEdited] = useState(false)
const previousApp = useRef(null)
useEffect(() => {
if (app === null) {
getApp(someAppId).then(setApp).catch(window.alert)
}
if (previousApp.current === null && app) {
previousApp.current = { ...app }
}
if (previousApp.current && app) {
_.isEqual(app, previousApp.current) ? setEdited(false) : setEdited(true)
}
}, [app])
For example I change the input value of app.design[0].title = 'testing' with the following code:
const updateItem = e => {
let newApp = { ...app }
let { name, value } = e.target
newApp.design[0][name] = value
setApp(newApp)
}
This works in the sense that it updates my app state object but not that it detects any changes in comparison to previousApp.current
When I console log app and previousApp after changing both values, they are identical. Its seems to update my previousApp value aswell for some reason.
Rearranging the design array works as expected and detects a change with the following function:
const updateDesign = e => {
let newApp = { ...app }
newApp.design = e
setApp(newApp)
}
It probably wasn't detecting any difference because the array present in the design property was keeping the same reference.
When you do this:
let newApp = { ...app }
You're creating a new object for your app, but the property values of that object will remain the same, because it's a shallow copy. Thus, the design array (which is an object, and therefore is handled by reference) will keep its value (the reference) unaltered.
I think this would also solve your problem:
const updateItem = e => {
let newApp = { ...app };
let newDesign = Array.from(app.design); // THIS CREATES A NEW ARRAY REFERENCE FOR THE 'design' PROPERTY
let { name, value } = e.target;
newDesign[0][name] = value;
setApp({
...newApp,
design: newDesign
});
}
I found that using lodash's cloneDeep function i was able to remove the strange link that was occuring between previousApp and App; Even though I used {...app}
See the following:
if (previousApp.current === null && app) {
previousApp.current = cloneDeep(app)
}

change of one value in the state react

In the state I have data array that look like this
{
"offers" : [
{
"model": "shoes",
"availability": false,
"id": 1
},
{
"model": "t-shirt",
"availability": true,
"id": 2
},
{
"make": "belt",
"availability": false,
"id": 3
}
]
}
My task is to change the accessibility field. I have a button - change the availability. After clicking it I want to make the availability of a single field changed
changeAvailability = (ailability, id) => {
console.log(ailability)
console.log(id)
const { data } = this.state;
let newData = [...data]
this.setState({
data: newData.map(x => x.id === id ? x.ailability = !ailability : ailability )
})
}
I have created a function that does not work.
My idea was: I click on the element, pass to the element's id and accessibility. In this function, I create a copy of the state. In the right place, I change the availability and I am sending everything to the state.
This function does not work, nothing happens, I have no idea how to fix it
Seems like the map function is returning nothing ...
Could be something like this
changeAvailability = (ailability, id) => {
console.log(ailability)
console.log(id)
const { data } = this.state;
let newData = [...data]
const updateItem = (x) => {
if (x.id === id) {
x.ailability = !ailability;
}
return x;
};
this.setState({
data: newData.map(updateItem)
})
}
I am assuming you want to click on an element which passes an id and availability of an offer, find a matching entry (on the passed id) in the offers array in the state and change the availability of the found object to what you passed in the function.
I recommend using the callback that this.setState() allows you to use, providing a reference to a copy of the previous state, before the setState was called.
changeAvailability = (passedAvail, passedID) => {
this.setState((prevState) => {
// Make a copy of the offers array in the state
const offersArray = prevState.offers
// Find index of matching obj by id
const matchObjIndex = offersArray.findIndex(
(offerObj) => (
offerObj.id === passedID
)
)
// If a match was indeed found
if (matchObjIndex > -1) {
// Find matching obj from index obtained
const matchObj = offersArray[matchObjIndex]
// Change the availibility of the matched obj and obtain a modified obj
const modmatchObj = Object.assign(matchObj, {
availability: passedAvail
})
// Replace the offer obj by index in the offers array.
offersArray.splice(matchObjIndex, 1, modmatchObj);
}
// Finally set state with the updated offers lists
// If no match was found, it will return the old offers array
return {offers: offersArray }
})
}
You can try something like this to make it work:
changeAvailability = (availability, id) =>
this.setState(prevState => {
return {
offers: prevState.offers.map((e, i) =>
e.id !== id ? e : { ...prevState.offers[i], availability }
)
};
});
The issue with your solution is that you shouldn't modify the items you're iterating while doing a map, but you should just return whatever new data you wanna add.

Resources