Reactjs hooks: update value of map - reactjs

I'm new to reactjs and that's why it comes naive to you.
I need to update the value of a map in which its keys are unknown.
const App = () => {
const [storeMap, setStoreMap] = useState(new Map());
let _tmpMap = new Map();
return (<>
{Object.keys({ key1: "hey", key2: "you" }).map((item) => {
return (
<button
value={item}
key={item}
onClick={(e) => {
_tmpMap.set(item, e.target.value);
console.log(..._tmpMap); // {1}
setStoreMap(_tmpMap);
}}
>
{item}
</button>
);
// return <i key={item}>KJ </i>;
})}
</>)
}
What I am expecting to see in the above code after clicking both buttons is:
/* {1} */
console.log(..._tmpMap)
//i expect this: {key1:"key1" , key2:"key2"}
What I see in reality is {key1:"key1"} after pressing key 1 and { key2:"key2"} after pressing key 2
My question is:
How can I update storeMap while preserving its previous entries?
Here is the code

Ciao, here working code. If you click key1 or key2 button, elements are added to the Map, if you click show result button you will see storeMap value.

When you call setStoreMap, the component rerenders and _tmpMap evaluates to a new Map again. The Map you updated belongs to the previous render and cannot be accessed. Anything you wish to preserve between renders has to be in state or a ref, so, you could do something like this:
const App = () => {
const [storeMap, setStoreMap] = useState(new Map());
let _tmpMap = useRef(new Map());
return (<>
{Object.keys({ key1: "hey", key2: "you" }).map((item) => {
return (
<button
value={item}
key={item}
onClick={(e) => {
_tmpMap.current.set(item, e.target.value);
setStoreMap(new Map(_tmpMap.current));
}}
>
{item}
</button>
);
// return <i key={item}>KJ </i>;
})}
</>)
}
However, it's generally advised not to use Maps with React as they are mutable, and React will have no way of knowing when one is mutated. The only way that storeMap will trigger rerenders and effects is if you set it to a new Map every time you update it. If you absolutely must use Maps, then mutable refs are the closest thing to them that React offers. An Object is about the most complex thing that belongs in React state. See this thread.

storeMap.set() updates the map and setStoreMap sets the state.
React compares the references of the new and old Map which, in this case, share the same value.
If you want React to "know" about the update you will need to pass to setStoreMap a clone of the Map instead of a copy of the old reference, you can do that by creating a new Map.
I believe you can drop the use of _tmpMap.
const App = () => {
const [storeMap, setStoreMap] = useState(new Map());
const updateStoreMap = (k, v) => {
// pass a clone of storeMap
setStoreMap(new Map(storeMap.set(k, v)));
};
return (
<>
{Object.keys({ key1: 'hey', key2: 'you' }).map((item) => {
return (
<button
value={item}
key={item}
onClick={(e) => {
updateStoreMap(item, e.target.value);
}}
>
{item}
</button>
);
// return <i key={item}>KJ </i>;
})}
</>
);
};

Related

Remove text by clicking on it - React

I'm trying to start learning react but fail understanding basic logic.
I have a todo list page, which works fine with a strike-through, but if I try to change the strike through to REMOVE instead, my app disappears on click.
Here's my code, hopefully you can understand:
function Note({ notes, note, onClickSetter }) {
const { input, id } = note
const [strikeThrough, setStrikeThrough] = useState(false);
function onNoteClick(event) {
const { value, id } = event.target
//setStrikeThrough((prev) => !prev) - the strike through which is canceled right now
onClickSetter(prev => prev.filter(aNote => aNote.id !== id)) // why this doesn't work?
}
return (
<>
<h1 style={ strikeThrough ? {textDecoration: 'line-through'} : { textDecoration: 'none' }} id={id} onClick={onNoteClick}>{input}</h1>
</>
)
}
a little explanation on my props:
notes - literally the list of notes which comes from a useState on father component (we shouldn't touch this from my understanding of react)
note - self note information
onClickSetter - the other part of useState, the setter one.
So on another words, I have the notes which holds all notes, and onClickSetter which is in another words is setNotes - both part of useState
on top of that I have a note information, because this is a note component
the father component:
function Body() {
const [Notes, setNotes] = useState([])
return (
<div className='notes-body'>
<NewNote onClickSetter={setNotes}/>
{Notes.map((note) => { return <Note key={note.id} notes={Notes} note={note} onClickSetter={setNotes}/>})}
</div>
)
}
function NewNote({ onClickSetter }) {
const [input, setInput] = useState('')
function onInputChange(event) {
const { value } = event.target
setInput(value)
}
function onButtonClick(event) {
onClickSetter((prev) => {
try {
return [...prev, {input: input, id: prev[prev.length-1].id+1}]
}catch{
return [{input: input, id: 0}]
}
})
setInput('')
}
return (
<>
<Input placeholder="add new note" className='note-text' onChange={onInputChange} value={input}/>
<Button className='btn btn-primary add-note' onClick={onButtonClick} />
</>
)
}
The reason is that event.target.id is a string representing a number since all HTML attributes has the string type. Whilst in your data structure, the ID is a number. So, e.g. "1" vs 1. This can be hard to spot sometimes.
The easiest way to fix this is to add a parseInt to the right place to convert the string to a number:
onClickSetter((prev) => prev.filter((aNote) => aNote.id !== parseInt(id)))
However, I also want to mention (and this is more advanced stuff but I like to get people on the right track :) ) that really, you shouldn't pass the whole setter down into the child component, but instead a callback called something like onRemoveNote that accept the note id and the actual filtering/removal would happen in the parent component.
This would be better placement of concerns. For now though, the above will work and I can help you out on stack overflow chat if needed :).

React not rendering list after the state is changed

I am creating a simple tracker which records all the activities done. It is my first project in react. I have created three state one for storing all the items(name of state is list), one for pending items(name of state is pending) , one for completed items(name of state is completed). The items have a button which when clicked marks it into done state and vice-versa. It is completely rendering items for main list. But for other two its not rendering. When I am checking with react developer tools, it is working fine, i.e. it is adding to pending list or completed list as it should. But it is not compiling them on screen. list is already filled with items. I have added all the code for just in case.
function Filters(props){
const [completed, setCompleted] = useState([]);
const [pending, setPending] = useState([]);
const [state, setState] = useState("None");
const [list,setList] = useState([]);
function viewState(){
setState("View-all");
}
//it is getting the clicked item id and marking it complete in main list
function markComplete(id){
list.map((items,index)=>{
if(index===id){
if(items.done===true)
items.done = false;
else{
items.done=true;
}
}
})
}
//i am simply scanning the main list and the items which are pending will be added to this list. //this happens whenever the person click on pending button
function pendingState(){
setState("pending-all");
setPending([]);
list.map(items=>{
if(items.done!==true){
setPending(prev=>{
return [...prev,items];
})
}
})
}
function completedState(){
setState("completed-all");
setCompleted([]);
list.map(items=>{
if(items.done===true){
setCompleted(prev=>{
return [...prev,items];
})
}
})
}
return (
<div>
<div className="input-section">
<Note setList={setList} />
</div>
<button type="button" onClick={viewState} >View All</button>
<button type="button" onClick={completedState}>Completed</button>
<button type="button" onClick={pendingState}>Pending</button>
<div>
{
(()=>{
if(state==="View-all")
{
return (
<div>
<h1>Working {completed}</h1>
{(list).map((items,index)=>
{
return (
<Excercise
key={index}
id={index}
title={items.name}
list={props.list}
setList={props.setList}
content={items.desp}
markComplete={markComplete}
/>
)
})}
</div>
)
}
else if(state==="completed-all")
{
return (
<div>
{completed.map((items,index)=>{
<Excercise
key={index}
id={index}
title={items.name}
list={props.list}
setList={props.setList}
content={items.desp}
markComplete={markComplete}
/>
})}
</div>
)
}
})()
}
</div>
</div>);
}
Kindly help. Thank you.
Hi #DREW
The function code :
function markComplete(id){
setList(lists=>{
lists.map(item=>{
return item.id===id ?{...item,done: !item.done} : (item);})
}
)
}
When I am using it instead of
const markComplete = (id) => {
setList((list) =>
list.map((item) =>
item.id === id
? {
...item,
done: !item.done
}
: item
)
);
};
it is showing, "Cannot read properties of undefined (reading 'filter')"
arent the both same. If not, what am I doing wrong. Sorry for bugging so many times, I have just started with react.
I think you've overcomplicated things a bit. You only need one array to store the exercises in, the "pending" and "completed" states are easily derived from the list state and the state filter state value.
Issues
markComplete callback is mutating the list state. When updating the list state not only is a new array reference necessary, but also new element object references are necessary for the elements that are being updated.
Uses poor boolean comparisons to set a boolean value. You can either toggle a boolean or set the value to the result of a boolean expression.
Use the viewState, pendingState, and completedState handlers to simply set the filter value, and then derive the computed state when rendering by adding an inline filter function.
Use the exercise id property as a React key and as the property used for toggling the completed (done) state.
Solution
function Filters(props) {
const [state, setState] = useState("None");
const [list, setList] = useState([
...
]);
function viewState() {
setState("View-all");
}
function pendingState() {
setState("pending-all");
}
function completedState() {
setState("completed-all");
}
const markComplete = (id) => {
setList((list) =>
list.map((item) =>
item.id === id
? {
...item,
done: !item.done
}
: item
)
);
};
return (
<div>
<div className="input-section">
<Note setList={setList} />
</div>
<button type="button" onClick={viewState}>
View All
</button>
<button type="button" onClick={completedState}>
Completed
</button>
<button type="button" onClick={pendingState}>
Pending
</button>
<div>
{list
.filter((item) => {
if (state === "pending-all") {
return !item.done;
} else if (state === "completed-all") {
return item.done;
}
return true;
})
.map((item) => (
<Excercise
key={item.id}
id={item.id}
done={item.done}
title={item.name}
content={item.desp}
markComplete={markComplete}
/>
))}
</div>
</div>
);
}
try to add dependecies in useEffect
in this function you are mutating a state, so in order to do so you need to use the setState function, in this case, it will be setList().
function markComplete(id){
list.map((items,index)=>{
if(index===id){
if(items.done===true)
items.done = false;
else{
items.done=true;
}
}
})
}
So a better way to implement this function could be, and remember, everything time you need to update a state you don't want to change the state directly, instead, you should make a copy and set the state to that copy
function markComplete(id){
const newList = [...list];
newList.map((items,index)=>{
if(index===id){
if(items.done===true)
items.done = false;
else{
items.done=true;
}
}
}
setList(newList)
}
The reason of your app not updating is because when your state changes react is not re-rendering it again.
so use useEffect, there are many hooks which can be used as per requirement.
try putting this line of code
useEffect( ( ) => {
console.log( 'Check console' );
}, [ dependency_here ] );
in dependency_here try adding state, completed, pending one by one and see the result.
You can also add multiple dependencies like [ state, pending, etc.. ];
Try on your own you'll understand it faster.
Hope hint will help you!

I want only one component state to be true between multiple components

I am calling components as folloews
{userAddresses.map((useraddress, index) => {
return (
<div key={index}>
<Address useraddress={useraddress} />
</div>
);
})}
Their state:
const [showEditAddress, setShowEditAddress] = useState(false);
and this is how I am handling their states
const switchEditAddress = () => {
if (showEditAddress === false) {
setShowEditAddress(true);
} else {
setShowEditAddress(false);
}
};
Well, it's better if you want to toggle between true and false to use the state inside useEffect hook in react.
useEffect will render the component every time and will get into your condition to set the state true or false.
In your case, you can try the following:
useEffect(() => { if (showEditAddress === false) {
setShowEditAddress(true);
} else {
setShowEditAddress(false);
} }, [showEditAddress])
By using useEffect you will be able to reset the boolean as your condition.
Also find the link below to react more about useEffect.
https://reactjs.org/docs/hooks-effect.html
It would be best in my opinion to keep your point of truth in the parent component and you need to figure out what the point of truth should be. If you only want one component to be editing at a time then I would just identify the address you want to edit in the parent component and go from there. It would be best if you gave each address a unique id but you can use the index as well. You could do something like the following:
UserAddress Component
const UserAddress = ({index, editIndex, setEditIndex, userAddress}) => {
return(
<div>
{userAddress}
<button onClick={() => setEditIndex(index)}>Edit</button>
{editIndex === index && <div style={{color: 'green'}}>Your editing {userAddress}</div>}
</div>
)
}
Parent Component
const UserAddresses = () => {
const addresses = ['120 n 10th st', '650 s 41 st', '4456 Birch ave']
const [editIndex, setEditIndex] = useState(null)
return userAddresses.map((userAddress, index) => <UserAddress key={index} index={index} editIndex={editIndex} setEditIndex={setEditIndex} userAddress={userAddress}/>;
}
Since you didn't post the actual components I can only give you example components but this should give you an idea of how to achieve what you want.

State update doesn't re-render component in ReactJS

I have a component in which I have this useEffect:
const [charactersInfo, setCharactersInfo] = useState(null);
const [page, setPage] = useState(1);
useEffect(() => {
fetch(`https://api/api/character/?page=${page}`)
.then((res) => res.json())
.then((result) => {
setCharactersInfo(result);
});
}, [page]);
whenever my page state updates there is different data coming from the api as expected. but issue is whenever new setCharactersInfo(result) happens, it does not display the new data.
I am passing my setPage state function to this component as a prop:
<PaginationButtons
data={charactersInfo}
updatePage={(number) => {
setPage(number);
}}
/>
This is re-usable component which generates buttons and it works correctly everywhere except this specific component. any suggestions please?
import React, { useState, useEffect } from "react";
import "./PaginationButtons.css";
function PaginationButtons({ data, updatePage }) {
const [buttonsArr, setButtonsArr] = useState([]);
useEffect(() => {
const finalArray = [];
const { info } = data;
// Not the best solution for situations in which
// info.pages is big number(e.x 1000000) but since we know that
// it mostly will be 34 or less so we can just loop through it :)
for (let i = 1; i < info.pages + 1; i++) {
finalArray.push(
<button
className="page_button"
onClick={() => updatePage(i)}
key={Math.random()}
>
{i}
</button>
);
}
setButtonsArr(finalArray);
}, []);
return <div className="button_container">{buttonsArr.map((el) => el)}</div>;
}
export default PaginationButtons;
data prop is an object which contains various of stuff and on the them is the number of pages that should be displayed. in this specific case it 34 for so I use state and useEffect to loop through this number and store buttons in the state array and map it afterwards
You should handle data change in your child component as well.
pass data to useEffect dependency list.
useEffect(() => {
const finalArray = [];
const { info } = data;
// Not the best solution for situations in which
// info.pages is big number(e.x 1000000) but since we know that
// it mostly will be 34 or less so we can just loop through it :)
for (let i = 1; i < info.pages + 1; i++) {
finalArray.push(
<button
className="page_button"
onClick={() => updatePage(i)}
key={Math.random()}
>
{i}
</button>
);
}
setButtonsArr(finalArray);
}, [data]);
This should help you, no need to maintain state. and i see pages is not array its just key value pair.
function PaginationButtons({ data, updatePage }) {
const { info : { pages } } = data;
return (
<div className="button_container">
<button
className="page_button"
onClick={() => updatePage(pages || 0)}
key={pages}
>
{pages || 0}
</button>
</div>
);
}
The useEffect in PaginationButtons is using an empty dependency so it doesn't update when the data prop updates. From what I can tell you don't need the buttonsArr state anyway. It's also anti-pattern to store derived state from props. Just map the data array prop to the buttons.
Using random values is probably the least ideal method of specifying React keys. You can use the mapped array index, but you should use a unique property for each page element, or if there isn't one you should augment the data with a generated GUID.
function PaginationButtons({ data, updatePage }) {
return (
<div className="button_container">
{data.?info?.pages?.map((page, i) => (
<button
className="page_button"
onClick={() => updatePage(i)}
key={i} // <-- or page.id if available
>
{i}
</button>
))}
</div>
);
}
As stated in the other answers, you need to add data as a dependency. Also, you don't need to call map on buttonsArr, as you're not doing anything with its elements. Just use buttonsArr itself

Unnecessary deleting elements from dom without making an action on this elements

import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const CreateCriteria = () => {
const [criteria, setCriteria] = useState({})
const onClose = useCallback((key) => {
delete criteria[key]
setCriteria(criteria)
}, [criteria])
const onClick = useCallback(() =>{
const key = Math.random()
setCriteria({
...criteria,
[key]: <div onClick={() => onClose(key)}>{
key
}</div>
})
}, [criteria, onClose])
return (
<div>
{
Object.keys(criteria).map((entity) => criteria[entity])
}
<button onClick={onClick}>
add
</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<CreateCriteria />, rootElement);
Imagine that I have this snippet of code. When I click on add button I want to get new elements in the DOM and in the criteria object. I do it by generating new par ( key/value ) in criteria object and then parse this new data in DOM. I do it because I want also to have the opportunity to delete this object from dom by clicking on this object. When I click on elements from last to first ( from below to above), then it works fine, but for example, if I have 3 elements 1,2,3 and I click on 2, then it also delete 3, and I can't find out why does this happen. You can use CodeSandbox for checking it :
https://codesandbox.io
You did two errors
You assidetially created closure when adding new keys in creteria object. This code
[key]: <div onClick={() => onClose(key)}>{
key
}</div>
not only adds <div> element to creteria object but create closure with function onClose. onClose in its turn create includes in closure creteria object as it is in time of adding new key in creteria object. So when onClose called, it has only keys that was there when new key added. So you see strange behaviour.
To solve problem I suggest to store only keys in creteria object and encapsulate them in <div> during render.
Another problem pointed in comments. You should create new creteria object in onClose. Otherwise React will not update creterai as it only compares new and old state using Object.is
To solve this, use destruct operator to create new copy of creteria object like this
const onClose = useCallback((key) => {
let newCreteria = { ...criteria };
delete newCreteria[key];
setCriteria(newCreteria);
}, [criteria])
Also useCallback is useless. But code works with them in palce.
Working sample is here
const CreateCriteria = () => {
const [criteria, setCriteria] = useState({});
const deleteCriteria = useCallback(
key => {
const newCreteria = { ...criteria };
delete newCreteria[key];
setCriteria(newCreteria);
},
[criteria]
);
const onClick = useCallback(() => {
const key = Math.random();
setCriteria({
...criteria,
[key]: null
});
}, [criteria]);
const CriteriaList = useMemo(() => (
Object.keys(criteria).map(entity => (
<div key={entity} onClick={() => deleteCriteria(entity)}>
{entity}
</div>
))
), [criteria, deleteCriteria])
return (
<div>
{CriteriaList}
<button
onClick={onClick}
>
add
</button>
</div>
);
};

Resources