I have to update a state of items and see the changes right away. When I'm doing this below, I get an infinite loop.
useEffect(() => {
const playersInFight = gameData.characters.filter((x) =>
fightData.currentPositionInfoDTOS.some((y) => y.entityId == x.id)
);
let orderedCharacters = addPlayers(
playersInFight,
fightData.currentPositionInfoDTOS,
fightData.turnOrder
);
setAllCharacterCards(orderedCharacters);
}, [fightData, gameData, fightInfo, allCharacterCards, allCharacters]);
I have tried also like this:
useEffect(() => {
const playersInFight = gameData.characters.filter((x) =>
fightData.currentPositionInfoDTOS.some((y) => y.entityId == x.id)
);
let orderedCharacters = addPlayers(
playersInFight,
fightData.currentPositionInfoDTOS,
fightData.turnOrder
);
setAllCharacterCards(orderedCharacters);
}, [fightData, gameData, fightInfo, allCharacterCards, allCharacters]);
useEffect(() => {
setAllCharacterCards(allCharacterCards);
}, [allCharacterCards]);
And thanks to that I don't have an infinite loop, but to see the changes I have to refresh the page...
How can I solve this?
Update:
Here is my return:
return (
<section>
<div className="player-fight">
{allCharacterCards ? (
<div>
{allCharacterCards.map((c, idx) => {
return (
<li key={idx} className="player-fight-bottom__player-card">
<CardComponentPlayerCard
id={c.id}
name={c.name}
money={c.money}
health={c.health}
maxHealth={c.maxHealth}
mine={false}
description={c.description}
statuses={c.statuses}
image={c.photoPath}
/>
</li>
);
})}
</div>
) : (
<LoadingSpinner />
)}
</div>
</section>
);
To avoid infinite looping you need to remove allCharacterCards from useEffect dependency array.
useEffect(() => {
const playersInFight = gameData.characters.filter((x) =>
fightData.currentPositionInfoDTOS.some((y) => y.entityId == x.id)
);
let orderedCharacters = addPlayers(
playersInFight,
fightData.currentPositionInfoDTOS,
fightData.turnOrder
);
setAllCharacterCards([...orderedCharacters]);
}, [fightData, gameData, fightInfo, allCharacters]);
Explanation (after question edited)
setAllCharacterCards([...orderedCharacters]);
This should fix your problem.
Since allCharacterCards is an array of objects you need to use spread operator ... to let react know that you are updating state everytime with new values.
Read more about - Deep copy vs shallow copy.
Related
In the code snippet below, I'd like to move this function out of jsx and wrap into useCallback.
{suggestedTags.length ? (
<div className={classes.tagSuggestionWrapper}>
{suggestedTags.map((tag) => {
return (<div key={tag}
onClick={() => { selectTag(tag) }}>{tag}</div>
);
})}
</div>
) : null }
Otherwise, a new function is created for every element on every render.
I understand that this may complicate the code, and may not be advisable. But I have to do it. I ask for your advice
You can do:
const selectTag = useCallback((tag) => {
setTags((prevState) => [...prevState, tag]);
setSuggestedTags([]);
setInput("");
}, [])
Little about useCallback
Bare in mind that if you had used any state variable, or prop, you should have included that in the dependency array. An empty dependency array makes sure selectTag reference will stay the same once the component mounts.
And no dependency array is the same as not using useCallback at all
Removing the arrow function
You can remove the arrow function by passing the value by using the onClick event function:
const selectTag = (event) => {
const tag = event.target.name
setTags((prevState) => [...prevState, tag]);
setSuggestedTags([]);
setInput("");
}
return (
{suggestedTags.length ? (
<div className={classes.tagSuggestionWrapper}>
{suggestedTags.map((tag) => {
return (<div key={tag}
name={tag}
className={classes.tagSuggestion}
onClick={selectTag}>{tag}</div>
);
})}
</div>
) : null }
</div>
);
I am trying to refactor my code and in doing so, I am extracting a single item and putting it into its own component. This MemberItem component has multiple functions state that influence its rendering, however, when I start passing props, the component breaks. I am passing all of the functions, properties and state the the child component needs, but I am still unable to get it to render properly.
// Members.js (Parent Component)
export const Members = () => {
// BELOW ARE THE FUNCTIONS AND STATE THAT INFLUENCE THE CHILD COMPONENT
const [memberName, setMemberName] = useState('')
const [editingMemberName, setEditingMemberName] = useState(
members.map(() => false)
)
// Update member name
const editMemberName = async (_, index) => {
let new_editing_members_state = members.map(() => false)
new_editing_members_state[index] = true
setEditingMemberName(new_editing_members_state)
}
// Cancel editing mode
const cancelEditMemberName = async (_, index) => {
let new_editing_members_state = members.map(() => false)
new_editing_members_state[index] = false
setEditingMemberName(new_editing_members_state)
}
// UPDATE name in database
const updateMemberName = async (index, id) => {
let new_editing_members_state = members.map(() => false)
new_editing_members_state[index] = false
setEditingMemberName(new_editing_members_state)
}
// BELOW, LOOPS OVER EACH ITEM
const memberItems = members.map((member, index) => {
return (
<MemberItem
member={member}
index={index}
editingMemberName={editingMemberName[index]}
editMemberName={editMemberName}
handleChangeName={handleChangeName}
updateMemberName={updateMemberName}
cancelEditMemberName={cancelEditMemberName}
destroyMember={destroyMember}
/>
)
})
return (
// RENDER THE LIST OF ITEMS
{memberItems}
)
}
// Member.js (Child Component)
export const MemberItem = (
member,
index,
editingMemberName,
editMemberName,
handleChangeName,
updateMemberName,
cancelEditMemberName,
destroyMember
) => {
return (
<div
key={member.id}
>
<div>
{editingMemberName[index] ? (
<input
type="text"
placeholder="Johnny Appleseed"
onChange={handleChangeName}
/>
) : (
<>
<div>
{member.name.substring(0, 1).toUpperCase()}
</div>
<h3>{member.name}</h3>
</>
)}
</div>
<div>
{editingMemberName[index] ? (
<button
onClick={() => updateMemberName(index, member.id)}
>
<CgCheckO size=".75em" />
</button>
) : (
<button
onClick={() => editMemberName(member.id, index)}
>
<FiTool size=".75em" />
</button>
)}
<button>
{editingMemberName[index] ? (
<GiCancel
onClick={() => cancelEditMemberName(member.id, index)}
size=".75em"
/>
) : (
<RiDeleteBinLine
onClick={() => destroyMember(member.id)}
size=".75em"
/>
)}
</button>
</div>
</div>
)
}
Currently, I am getting an error of TypeError: editingMemberName is undefined and a warning of Each child in a list should have a unique "key" prop, but if you see, I do pass in an id into the key property.
In React, props are passed down to function components as a single object.
Your component function assumes props are passed down as separate arguments and not in a single object.
Fixed component definition (note the brackets around the argument list):
MemberItem = ({
member,
index,
editingMemberName,
editMemberName,
handleChangeName,
updateMemberName,
cancelEditMemberName,
destroyMember
}) => { ... }
This method of unpacking properties is called object destructuring.
I've a component call KeywordLocation.js, and It has one prop named location.
this component is a mapped array and on click I want to save the object of location in localStorage. I created here an empty array and pushing the object on every click. For now I'm getting 5 mapped location objects. when I click on any of them, it saves the object but on 2nd click it doesn't stop duplicating the object. How do I stop this duplication?
searchedLocation.map((location, i) => {
return (
<KeywordLocation
setShowMap={props.setShowMap}
location={location}
key={i}
getPositionFromManualSearch={props.getPositionFromManualSearch}
/>
);
});
KeywordLocation.js
const Component = ({ location }) => {
let allSearchedLocations = [];
const redirectToMap = async () => {
allSearchedLocations.push(location);
allSearchedLocations = allSearchedLocations.concat(
JSON.parse(localStorage.getItem("recent_location_searched") || "[]")
);
const previousLocation = JSON.parse(
localStorage.getItem("recent_location_searched")
);
console.log(previousLocation);
localStorage.setItem(
"recent_location_searched",
JSON.stringify(allSearchedLocations)
);
};
return (
<div onClick={() => redirectToMap()} className="pt-md cursor-pointer">
<p>{location.structured_formatting.main_text}</p>
<p className="text-xs border-b border-black pb-md ">
{location.description}
</p>
</div>
);
};
Are you entirely sure the duplication is ocurring on local storage?
As long as you use the same key, recent_location_searched, there will be only one value stored on that key. Take a look at the "Storage" tab on your browser's debug console to see what's actually being stored.
All evidence seems to point that the duplication is ocurring at the searchLocations variable, not atlocalStorage.
You might try to add some conditional logic that prevents you from pushing to searchLocations if the location is the same as the one on the last item on the array.
The problem is not related to localStorage but more about the usage of the array structure. You could rely on JavaScripts object to store the unique values. You lose the insertion order but you can create a companion array that keep a reference to the order.
const Test = ({ location }) => {
const redirectToMap = () => {
const locations =
JSON.parse(localStorage.getItem("recent_location_searched")) || {};
locations[location.name] = location;
localStorage.setItem("recent_location_searched", JSON.stringify(locations));
};
return (
<div onClick={() => redirectToMap()} className="pt-md cursor-pointer">
<p>{location.name}</p>
</div>
);
};
export default function App() {
const data =
JSON.parse(localStorage.getItem("recent_location_searched")) || {};
return (
<div>
<div className="App">
{[
{ name: "location1" },
{ name: "location3" },
{ name: "location2" }
].map((location) => (
<Test key={location.name} location={location} />
))}
</div>
<ul>
{Object.values(data).map((location) => (
<li key={location.name}>Saved {location.name}</li>
))}
</ul>
</div>
);
}
I have a list of items (I get the items with a GET-call - I didn't add it here, because I think it's irrelevant). When I delete an item, the list should be updated/ re-rendered.
To do this I use the useEffect-hook with a second parameter (productData).
Problem:
I have to refresh the page manually in order to see the new list of items. I don't understand why: I use useEffect with the second parameter. Could someone point me in the right direction what is wrong? Thanks a lot!
Here is my code:
export default function MemberSavedProducts() {
const [productData, setProductData] = useState([]);
const [successMessage, setSuccessMessage] = useState();
const [errorMessage, setErrorMessage] = useState();
useEffect(() => {}, [productData]);
const deleteProduct = async(prod) => {
try {
if (window.confirm("Are you sure you want to delete this product?")) {
const {
data
} = await fetchContext.authAxios.delete(
`savedProducts/${prod}`
);
setProductData(
productData.filter((prod) => prod !== data.deletedItem.Id)
);
setSuccessMessage(data.message);
}
} catch (err) {
const {
data
} = err.response;
setErrorMessage(data.message);
}
};
return (
<CardLarge>
<div className={styles.productWrapper}>
{successMessage && <SuccessMessage text={successMessage} />}
{errorMessage && <ErrorMessage text={errorMessage} />}
{productData.map((prod) => {
return (
<div
key={prod.id}
>
<ProductItem
prod={prod}
onClick={() => {
getInfo(prod.information);
}}
/>
<button
onClick={() => {deleteProduct(prod.Id)}}
>
Delete
</button>
</div>
);
})}
</div>
</CardLarge>
);
}
Discussed this in the comments, posting this answer for completeness.
Are you sure the filter function works? It seems the refresh works because the GET response returns the right array. I think it should be productData.filter((prod) => prod.Id !== data.deletedItem.Id));. Because in your code you are comparing an object to a string.
Or you can use the passed parameter prod instead of the response maybe like this productData.filter((p) => p.Id !== prod));
Also a small clarification: useEffect does not cause a rerender, changing state does trigger a rerender. useEffect is just a listener/callback that triggers on change of the declared dependencies.
(Apologies if some of my terms aren't correct)
In Firebase I have a number of posts. Each post has a 'latitude' field and a 'longitude' field. I am pulling these out and storing them in an array/object called mapRefs:
useEffect(() => {
projectFirestore.collection("posts").get().then(res => {
let mapRefs = [];
res.forEach(data => {
mapRefs.push([data.data().myLatitude, data.data().myLongitude]);
});
console.log(mapRefs);
});
}, []);
This works, the output for the console log is:
0: (2) [-8.6848548, 115.22303799999999]
1: (2) [-8.7848548, 115.323038]
2: (2) [-8.9848548, 115.52303799999999]
3: (2) [-8.8848548, 115.42303799999999]
How do I then iterate through these and map a latitude and longitude value to a component. I was trying like this:
<ReactMapGL>
{ mapRefs && mapRefs.map(coord => (
<Marker latitude={coord[0]} longitude={coord[1]}>
<div>
...
</div>
</Marker>
))}
</ReactMapGL>
This isn't working. What would be the correct way to do this, please?
You need use state values to render the UI elements and mapRefs is not available outside useEffect.
try like this
const [mapRefs, setMapRefs] = useState([])
useEffect(() => {
projectFirestore.collection("posts").get().then(res => {
let refs = [];
res.forEach(data => {
refs.push([data.data().myLatitude, data.data().myLongitude]);
});
setMapRefs(refs)
});
}, []);
return (
<ReactMapGL>
{ mapRefs.map(coord => (
<Marker latitude={coord[0]} longitude={coord[1]}>
<div>
...
</div>
</Marker>
))}
</ReactMapGL>
)