I'm trying to implement a dictionary in my React project. I'm guessing it's basic javascript so if you're not familiar with React you might still be able to help.
My goal is to have a dict which contains questions, where each question has an array of answers. I want to be able to add a question first, and then add answers later.
I can add the record but then I'm having trouble modifying the answer array:
Initial
const [dict, setDict] = React.useState([])
Adding dictionary record
question = "question1"
const newDict = dict.concat({ key: question1, value: [] });
setDict(newDict);
Modifying dictionary value
const answer = "valueToAdd"
const newDict = dict;
newDict["test"].concat(answer);
setDict(newDict);
I'm getting the following error. Seems like newDict["test"] is undefined, even though I just added it. What am I doing wrong?
TypeError: Cannot read property 'concat' of undefined
Also, is this the correct way to append to a dictionary? I'm doing it like this so dictionary will re-render.
I believe you are setting the initial value of your dict state variable to an empty array rather than a true dictionary like you may be intending. Javascript objects behave very similarly to dictionaries in other languages, so you may want to use an object for this instead.
Declaring your state
const [dict,setDict] = useState({})
Here we are initializing the state (dict) to an empty javascript object. These objects behave similarly to the dictionaries you are familiar with in other languages.
Adding a key value pair
setDict(prevDict => ({...prevDict, newKey: []}))
Here we are using an arrow function to provide the previous state to the object we will be using to update the state. This is done to keep the previous state immutable. The spread/rest operator "..." is being used to collect all of the values of the previous state, then add the new key-value pair
Updating Values
setDict(prevDict => ({...prevDict, keyToUpdate: [...prevDict.keyToChange, "newValue"]}))
Similar to the above, the spread/rest operator is being used, this time in two places. First to retain the keys of the previous state, second to keep all of the values from the array that we are going to be adding to.
Your dict can be an object literal.
//function that creates a unique key every time
const key = ((key) => () => key++)(1);
//item is pure component, won't re render unless props change
const Item = React.memo(function Item({ addValue, item }) {
return (
<div>
<pre>{JSON.stringify(item, undefined, 2)}</pre>
<button
onClick={() => addValue(item.key, Date.now())}
>
add random value
</button>
</div>
);
});
const App = () => {
//dict local state
const [dict, setDict] = React.useState({});
//add item to dict using useCallback so function
// addDict is only created whe App mounts
const addDict = React.useCallback(
() =>
//callback to setDict prevent dict to be a dependency
// of useCallback
setDict((dict) => {
const k = key();
return {
...dict,//copy dict
[k]: { key: k, values: [] },//set k with new object
};
}),
[]//no dependency
);
//function to add value to dict use useCallback again
// so function is only created when App mounts
const addValue = React.useCallback(
(key, value) =>
//pass callback to setDict to prevent dependency
setDict((dict) => ({
...dict,//copy dict
[key]: {//set this key
...dict[key],//copy dict[key]
values: dict[key].values.concat(value),//add value
},
})),
[]
);
return (
<div>
<div>
<button onClick={addDict}>add dict</button>
</div>
<div>
<ul>
{Object.values(dict).map((dict) => (
<Item
key={dict.key}
addValue={addValue}
item={dict}
/>
))}
</ul>
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
More info on updating immutable state can be found here
Related
This question already has answers here:
Array.fill(Array) creates copies by references not by value [duplicate]
(3 answers)
Closed 8 months ago.
I have nested arrays of empty strings inside a React state.
function App() {
const emptyWord = Array(5).fill("");
const initialArr = Array(5).fill(emptyWord);
const [words, setWords] = useState(initialArr);
I am trying to update them like this (the indexes are just an example):
const handleInput = input => {
const currWords = words;
currWords[0][2] = input;
setCurrLetter(input);
setWords(currWords);
}
But instead of updating just the array with index 0 in words, this updates every array. I tried different approaches, also using the spread operator, and can't get this to behave properly.
Any ideas?
There are a few issues with your code. The issue you're facing is that .fill() populates your array with the same emptyWord array refrence for each index. You want to change this so that you create a new unique inner arrays for each element, which you can do with Array.from() and it's mapping function:
const [words, setWords] = useState(() => Array.from(
{length: 5}, () => Array(5).fill("")
));
You'll notice that I've put the array creation logic inside of useState() hook so that you don't recreate your array needlessly every time App rerenders.
Now that you have unique arrays in each index, you won't experience the issue of having each array updated each time you update one of them.
However, your code still has an issue where you are mutating your state directly, as const currWords = words; doesn't make a copy of your array. Modifying your state directly can cause rendering issues where React doesn't update your UI to reflect the new state. To immutably update your state you should be creating a new array with a new inner element for the item you want to update, and not updating your state directly. This can be done with .map() for example:
const handleInput = input => {
const outerIdx = 0, innerIdx = 2;
setCurrLetter(input);
setWords(currWords => currWords.map((inner, i) => i === outerIdx
? inner.map((val, j) => j === innerIdx ? input : val)
: inner
));
}
You could also make a deep copy with JSON.stringify() + JSON.parse()1, but it wouldn't be as efficient as the .map() option shown above:
const currWords = JSON.parse(JSON.stringify(words));
currWords[0][2] = input;
setCurrLetter(input);
setWords(currWords);
1: If you can support it, you can use structuredClone() instead.
I'm working on a text editor using React and I want to keep track of the changes in an array. Whenever I make changes an object is added to the array (as it should) but all the other objects change as well and become the same as the new one. I'm aware of how Javascript doesn't store objects in independent variables if not reassigned so I used the spread operator to create a new array and then add a new object using Object.assign() but it's still not working and I can't figure out what I'm doing wrong.
getNewChangesHistory(update, oldChangesHistory){
var newChangesHistory = [...oldChangesHistory, Object.assign({}, update)];
if(newChangesHistory.length > 25){
delete(newChangesHistory[26]);
}
return newChangesHistory;
}
updateDocumentContent(content){
var newDocument = {...this.state.document};
newDocument.content = content;
this.setState(prevState => {return {
document: newDocument,
changesHistory: this.getNewChangesHistory(content, prevState.changesHistory),
hasChanges: true
}})
}
updateTextbox(editedProperties, key){
const newDocumentContent = {...this.state.document.content};
newDocumentContent.textboxes[key] = { //Textboxes are stored as objects of an array
...editedProperties,
id: key
}
this.updateDocumentContent(newDocumentContent)
}
render(){
return(
<TextBox
onEdit={(editedProperties) => {this.updateTextbox(editedProperties, 0)}}
/>
)
}
The problem is in updateTextbox. With {...this.state.document.content} it only creates a shallow copy. In this copy the textboxes property will still reference the same object. And you mutate that object by the assignment to its [key] property. So that mutation will be seen in all objects that have that same textboxes object reference.
One way to get rid of this, is to treat textboxes as immutable, and do this:
updateTextbox(editedProperties, key){
const {content} = this.state.document;
const newDocumentContent = {
...content,
// create a new textboxes array
textboxes: Object.assign([], content.textboxes, {
[key]: {
...editedProperties,
id: key
}
})
};
this.updateDocumentContent(newDocumentContent);
}
I have cloned an array and I have an onChange function. On calling onChange function my original and cloned array is updating according to the updated onChange value. But I don't want to update my cloned array. How can I do this ?
My code -
const clonedArray = [...originalArray];
const onChange = (id:number, value: string): void => {
const arrayData = originalArray;
const selectedData = arrayData.find(
(data) => data.myId === id
);
if (selectedData) {
// updating my originalArray according to new changed value;
}
}
this onChange function is also updating my clonedArray. I don't wanna update my clonedArray. How can I do this ? What is the solution useMemo or something ? Can anyone tell me the solution for it ?
Issue
This issue is state object/array mutation. You are mutating elements in an array and seeing the mutations manifest in a "copy".
Firstly, const clonedArray = [...originalArray]; is only a shallow copy of the originalArray, not a clone. This means that other than the array reference itself, all the elements in clonedArray still refer to the same elements in originalArray.
Secondly, if later you are making changes in originalArray and you are seeing them manifest in clonedArray then you are definitely mutating the element references instead of creating new references.
Solution
You are looking for the Immutable Update Pattern. When updating state in React it is necessary to shallow copy not only the root object/array that is being updated, but all nested state as well. To help with endeavor, especially when updating arrays, you will want to also use functional state updates so you can correctly update from the previous state.
For this I'm assuming (based on a comment that originalArray was in state) that your state looks something like this:
const [originalArray, setOriginalArray] = useState([]);
The change handler/state update (just an example since I don't know your exact update requirements)
const onChange = (id: number, value: string): void => {
const selectedData = originalArray.find((data) => data.myId === id);
if (selectedData) {
// updating my originalArray according to new changed value
// Array.prototype.map shallow copies the array into a new array reference
setOriginalArray(originalArray => originalArray.map(data => data.myId === id
? { // <-- updating element into new object reference
...data, // <-- shallow copy previous data element
property: value // <-- update property with new value
}
: data // <-- not updating, pass previous object through
);
}
}
Once you are correctly updating the array elements, then no mutations will occur to anything that may also be referencing the state.
you can use useMemo with empty dependency array. whenever originalArray updates, clonedArray doesn't change.
import { useMemo, useState } from "react";
const arr = [1,2,3]
export default function App() {
const [originalArray, setOriginalArray] = useState(arr)
const clonedArray = useMemo(() => originalArray, [])
return (
<div className="App" style={{textAlign:'center'}}>
originalArray: {originalArray}
<br/>
clonedArray:{clonedArray}
<br/>
<button onClick={() => setOriginalArray([1])}>update original</button>
</div>
);
}
I am creating a dropdown filter to update the search results of a page using react hooks. Basically, I am passing an array with the options that the user chose from the dropdown menu. I am successfully updating the global state with the new arrays BUT my issue is useState creates a NEW array instead of merging the results with the previous state.
Above you can see, I made two calls with different filter options and the global state now holds 2 arrays. My goal is to have both arrays merged into one.
This is the function where the global state is being updated.
const Results = () => {
const [filterList, setFilterList] = useState([])
const setGlobalFilter = (newFilter) => {
let indexFilter = filterList.indexOf(newFilter);
// console.log("Top level index", indexFilter)
indexFilter ?
setFilterList([...new Set([...filterList, newFilter])]) :
setFilterList(filterList => filterList.filter((filter, i) => i !== indexFilter))
}
// console.log("TopFilterSelection:", filterList)
return (
<div>
<Filter setFilter={(filterList) => setGlobalFilter(filterList)}/>
</div>
)
}
I've been checking on using prevState like this:
...
setFilterList(prevState => [...new Set([...prevState, newFilter])]) :
...
But I don't know what I am doing wrong.
Any feedback would be much appreciated!
This happens because newFilteris an array, not a word.
Should be
setFilterList(previous => [...new Set([...previous, ...newFilter])])
Also this
let indexFilter = filterList.indexOf(newFilter);
always returns -1 if newFilteris an array (since you a sending brand new array each time), it's not a falsy value, be careful
Use the .concat method.
setFilterList(filterList.concat(newFilter))
Read more about it here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
Currently I have an array of objects with the form called pairIDCost
{"id": 1, cost: "10"}
And I want to pass the cost value into each component in the component array constructed from another array (array1) that looks something like this
const componentArray = array1.map(d => <Component cost={QUERIED_COST} data={...} />)
Instead of passing the entire array of objects into each component of the component list and doing the logic in the child component, I figured it would be more efficient to first query for the correct cost of each component.
I tried to pass a mess of nested function in place of QUERIED_COST so the inside of the .map function looks like
d => <Component cost={
() => {
const tmp = pairIDCost.find(element => element.id === d.array)
return tmp.cost
}
}
What is the proper way to go around this? Does this "optimization" even matter when running apps?
Yes you should query the value before passing it to each component. The way you suggest passes a function as prop to the child component, which is incorrect. Instead, do it inside the map loop:
const componentArray = array1.map(d => {
const tmp = pairIDCost.find(element => element.id === d.array);
return <Component cost={tmp && tmp.cost} data={...} />
});