React-Redux remove object of array synchronous - reactjs

I have the following example saved in a array in redux:
example: [
{ type: "A", value: 25 },
{ type: "B", value: 23 },
{ type: "C", value: 324 },
{ type: "A", value: 25 },
]
As you can see, some of the objects may appear multiple-times, this should not be changed.
Now i try to remove an specific objects from the list without using the index. This is quite easy, because i can check if the object is the object to remove based on the comparsion below:
example[0] == example[2] // => false
example[0] === example[2] // => false
example[0] == example[0] // => true
example[0] === example[0] // => true
The problem is the aynchronous behavior of [].filter which i use to remove a entry in my reducer as you can see implemented here:
case EXAMPLE.REMOVE:
return state.filter((obj) => obj !== action.payload.objectToRemove)
Because when i remove the entries very fast, it sometimes happen than a entry may was removed during the filter but another filter-function also currently runs and then the previously removed object from the other filter-function will be back in the state.
How can i ensure that the reducer works synchronous to avoid such racing-conditions or is there even a more elegant way?
PS: I'm quite sure that the asynchronous-behavior of redux is wanted and the right-way to do it, may i need an other
Edit: I was able to solve the problem due to the fact, that as described in the comments it was not caused by an async call of the reducer but by a rerender of each component caused by the fact that i've not assigned a key and all entries which came after the entry to remove were unmounted and mounted again in the ui were rendered a component per entry of the array. Seems like i missunderstood the real causing problem at this point.

Related

Why doesn't useState work with deeply nested objects and arrays in them?

In my use case I have an array of characters, each character has multiple builds, and each build has a weapons string, and artifacts string. I'm making a tool to select portions of each string and assign them to a value, e.g. assigning index 3-49 of weapons to a specific weapon.
const [characterIndices, setCharacterIndices] = useState<
{ builds: { weaponIndices: SE[]; artifactSetIndices: SE[] }[] }[]
>([
...characters.map((char) => {
return {
builds: [
...char.builds.map((_build) => {
return {
weaponIndices: [],
artifactSetIndices: [],
};
}),
],
};
}),
]);
The SE type is as follows:
type SE = { start: number; end: number; code: string };
//start and end are the respective start and end of selected text
//code is the specific artifact or weapon
The weaponIndices and artifactSetIndices basically hold the start and end of selected text in a readonly textarea.
I have a function to add a SE to either weaponIndices or artifactSetIndices:
const addSE = (
type: "weaponIndices" | "artifactSetIndices",
{ start, end, code }: SE,
characterIndex: number,
buildIndex: number
) => {
let chars = characterIndices;
chars[characterIndex].builds[buildIndex][type].push({ start, end, code });
setCharacterIndices((_prev) => chars);
console.log(characterIndices[characterIndex].builds[buildIndex][type]);
};
I think that using a console log after using a set function isn't recommended, but it does show what it's intended to the weaponIndices, or artifactSetIndices after an entry is added.
Passing the addSE function alongside characterIndices to a separate component, and using addSE, does print the respective indices after adding an entry, but the component's rendering isn't updated.
It only shows up when I "soft reload" the page, when updating the files during the create-react-app live reload via npm run start.
In case you are confused about what the data types are, I've made a github repo, at https://github.com/ChrisMGeo/ght-indexer/tree/main/src at src/data.json. That JSON file describes what the character data looks like, including the builds, and each build's weapons and artifacts(called artifact_sets in the JSON)
Looks to me you are not updating the state at all.
Here you are just storing the same object reference that you already have in state into a new variable chars.
let chars = characterIndices;
chars now holds reference to a same object as characterIndices.
Here you are mutating that same object
chars[characterIndex].builds[buildIndex][type].push({ start, end, code });
And here you are updating the state to the same object that is already in the state. Notice that no state update here occurs.
setCharacterIndices((_prev) => chars);
Object you have in state is mutated, but you did not "change" the value of the state, thus no component re-render.
What you could maybe do is create a copy of the object, mutate that and update the state. just change chars assignment like this:
let chars = {...characterIndices};
React often compares values using Object.is() only to a single level of nesting (the tested object and its children).
It will not re-render if the parent is found equal, or if all the children are found equal.
React then considers that nothing has changed.
In your implementation, even the first top-level check will immediately fail, since Object.is(before, after) will return true.
You could use an Immutable objects approach to eliminate this concern when setting a new state (either directly through spreading values or with a support library such as Immer).
For example instead of setting the values within the object...
myObj.key = newChildObj
...you would make a new object, which preserves many of the previous values.
myObj === {...myObj, key: newChildObj}
This means that every changed object tree is actually a different object (with only the bits that haven't changed being preserved).
To read more about this see https://javascript.plainenglish.io/the-effect-of-shallow-equality-in-react-85ae0287960c

React.js: How to properly append multiple items to an array state using its use-state hook setter?

My component has a state that should store an array of objects. I initialized it to be empty.
const [tableDataSource, setTableDataSource] = useState([])
After a couple successful fetching and reorganizing of fetch results, one or more object literals should be appended to the tableDataSource state. Desired output should be
[
{
key: 1,
key2: 'sample string value',
key3: false,
},
{
key: 'sample string value',
key2: true,
}
]
My current fetch result is another array containing 2 object literals. I tried to append all items from this array to the state setter.
let dataSource = generateRows(showCompletedItems, fetchResult);
The code above reorganizes the fetch results into desired output. I logged dataSource in the console and verified if it is an Array object and it returned true.
Somewhere inside the component declaration I also have a useEffect that logs the current tableDataSource to output the changes.
console.log('TABLE DATA SOURCE >>>>', tableDataSource, typeof tableDataSource);
I have a difficult time appending the array items into the state array using its setter. I tried Approach # 1:
setTableDataSource(dataSource);
The console does not log anything.
Approach # 2:
setTableDataSource((tableDataSource) => [...tableDataSource, dataSource])
Returns a nested array as shown below:
[[{...},{...}]]
If I used Approach # 2 and initialized the component state like this:
const [tableDataSource, setTableDataSource] = useState([{}])
Result:
[{}, [{...}, {...}]]
Approach # 3 does not log anything like the first one
setTableDataSource((tableDataSource) => [...tableDataSource, ...dataSource])
Figured it out.
setArrayOfObjects((currentObjects) => currentObjects.concat([ ...anotherArrayOfObjects]))
I hope this very specific problem will help others. It was very annoying, it took me hours to figure it out.
You can use Set.
setTableDataSource((tableDataSource) => [... new Set([...tableDataSource, ...dataSource])])
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set

how do I modify a state without overwriting it

I have a search bar, and once the user enters any word or words (eg: "green plant") I split those words with space and array would look like words = ["green","plant"]. Now, I need to use each word as a value to my filter api which indeed returns the data that has the search word in the title or name.
The issue is that, each time it calls it overwrites the existing state so that whatever data I got for word "green" is overwritten with the next api call for "plant".
My Code for setting them,
if (response.status === 200) {
setProduct(response.data.product)
}
I also tried to use spread operators,
if (response.status === 200) {
setProduct([...product,response.data.product])
}
and also tried,
if (response.status === 200) {
setProduct((prevProducts) => [
...prevProducts.product,
response.data.product,
]);
}
Error for the above TypeError: Invalid attempt to spread non-iterable instance
response.data.product:
[
{
"_id":"61daa6401d0f202659003c12",
"name":"TIMA - Top Class Blanket",
"originalPrice":1599,
"storePrice":1500,
"discount":6.2,
"description":"Single Blanket , Size: 226*150 cm",
"category":"blankets",
"quantity":10
},
{
"_id":"61daa6401d0f2026592eef3",
"name":"Flora Bed",
"originalPrice":2599,
"storePrice":1500,
"discount":7,
"description":"Bed , Size: 226*150 cm",
"category":"bed",
"quantity":10
}
]
You're most likely encountering a closure issue, where you are looping through the two API calls, but those get initialized with the same state A.
This would mean that when you want to update the state from A->B in the first call, then B->C in the second, what actually happens is that you get A->B followed by A->C, without the changes from B.
I recommend using the Promise.all (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) to await for all results, then set your state at once. If you really want to do it sequentially, then make sure to await the first one, set your state after it, then do the same with the second one. For this latter solution (not recommended), make sure to use the callback setProduct((prev) => [...prev, newValue]).

Allow temporarily empty object for a typed state

I'm coming from JS background and learning Typescript now. I can't get over this issue.
I have a very particularly typed state that I know I will need in the future to work with:
type NotificationValuesType = {
channel: string
for: NotificationUsageTypes
id: string
type: NotificationTypes
workspace: string
}[]
I'm setting my React state like this:
const [dropdownsState, setDropdownsState] = useState<NotificationValuesType>([])
The thing is that initially I can now only type and id, all of the rest of props I can gather thru a series of dropdowns when a user chooses them, event fires, and then populates the state with one dropdown at a time, so at some point, it will be only:
[{id: "id", type: "type", channel: "channel"}]
at the next event, it will be [{id: "id", type: "type", channel: "channel", workspace: "workspace"}] and one more step and state update to get to know all the props in the declared type.
What I don't understand is how to tell Typescript to stop yelling at me until the point I know all the props and make sure I will know all the needed props in the future.
I absolutely cannot make any of these props optional because they
aren't optional.
Also tried setting state type to NotificationValuesType | [] but typescript keeps yelling
I learned about type assertions and guess it should help but can't find any example of using it on the state. Can I do something
like const [dropdownsState, setDropdownsState] = useState as <NotificationValuesType>([]) ???
Thanks for reading all way to the end! =)
I absolutely cannot make any of these props optional because they
aren't optional.
Perhaps not when you're done, but while you're in the process of building it, if values need to be undefined, then the type needs to reflect that. So most likely you'll want to have the state variable have optional properties, then you go through your steps to fill it out, and once you've verified it's all there you can assign it to a variable with a type where the properties are all mandatory.
There is a helper type called Partial which will take in a type and produce a new one who's properties are optional.
// Note: this type is just the individual object, not the array that you have
type Example = {
channel: string
for: NotificationUsageTypes
id: string
type: NotificationTypes
workspace: string
}
const [dropdownsState, setDropdownsState] = useState<Partial<Example>[]>([])
// Later on, once you've verified that all the properties exist you can
// assert that it's done. I don't know exactly what your verification
// code will look like, but here's an example
if (dropdownsState.every(value => {
return value.channel && value.for && value.id && value.type && value.workspace
})) {
const finishedState = dropdownsState as Example[];
// do something with the finished state
}
EDIT: as pointed out in the comments, if you use a type guard, then typescript can narrow down the types and save you from having to reassign it:
if (dropdownsState.every((value): value is Example => {
return value.channel && value.for && value.id && value.type && value.workspace
})) {
// Inside this block, typescript knows that dropdownsState is an Example[]
}

prevent a runtime error when filtering an array that might not exist at certainty point?

I have this reducer state chunk, that is causing a runtime error
(TypeError: (intermediate value)(intermediate value)(intermediate value) is not iterable)
since, at a certain point, the object might not be available in the state, any idea how to prevent the run time from stumbling on the part of chunk that is not available (childGroup) at the current moment? and run it only when certain criteria are met I tried adding ?? [] before the filter but typescript errored out cannot call filter on never. the issue happens when I want to delete the parent group only when there is no child group! , if there is a child group works like charm.
// REMOVE GROUP
case types.DELETE__GROUP:
return {
...state,
parentGroup: {
...state.parentGroup,
[action.payload.parentId!]: state.parentGrou[action.payload.parentId!]?.filter((el) => el.id !== action.payload.id),
},
childGroup: {
...state.childGroup,
[action.payload.parentId!]: [...state.childGroup[action.payload.parentId!]?.filter((el) => el.parentId !== action.payload.id)],
},
};
Conditional indexed access is spelled ?.[] in both TypeScript and JavaScript. So in addition to the ?.filter you need to add ?. to your [action.payload.parentId!] index, making that entire line:
[action.payload.parentId!]:
[
...state.childGroup?.[action.payload.parentId!]?.filter((el) => el.parentId !== action.payload.id)
],
But consider in investing in some utilities around this to make it clearer what the actual business logic is (for example Immer.js)

Resources