I'm learning react. I am trying to sort a list based on name. The ShoppingList component is
const ShoppingList = () => {
const [items, setItems] = useState([]);
const data = [
{id: 1, name: 'Soda'},
{id: 2, name: 'ice'},
];
useEffect(() => {
setItems(data);
}, []);
const handleSort = () => {}
return ();
}
On a button click I'm trying to sort the data and display it.
<button onClick={() => handleSort()}>Sort by name</button>
Inside the handleSort() function
const sortItems = items.sort((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if(nameA < nameB)
return -1;
if(nameA > nameB)
return 1;
return 0;
});
console.log(sortItems);
setItems(sortItems);
The console.log(sortItems) shows the sorted array. But not rendering in the DOM.
Inside the return, I'm trying to display the sorted data in this format
<ul>
{items.map((item) => {
return (
<li key={item.id}>
<span>{item.name} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
);
})
}
</ul>
What i'm missing here?
I'd recommend deriving the sorted list of items with useMemo, so it's "derived state" dependent on the items array and the desired sort order.
Don't use useEffect for initial state. useState accepts a creator function for the initial state instead.
localeCompare is a neater way to return -1, 0, +1 for comparison.
[...items] (a shallow copy of items) is required, because .sort() sorts an array in-place.
const sortByName = (a, b) => a.name.toUpperCase().localeCompare(b.name.toUpperCase());
const ShoppingList = () => {
const [items, setItems] = useState(() => [
{ id: 1, name: "Soda" },
{ id: 2, name: "ice" },
]);
const [sortOrder, setSortOrder] = useState("original");
const sortedItems = React.useMemo(() => {
switch (sortOrder) {
case "byName":
return [...items].sort(sortByName);
default:
return items;
}
}, [items, sortOrder]);
return (
<>
<button onClick={() => setSortOrder("byName")}>Sort by name</button>
<button onClick={() => setSortOrder("original")}>Sort in original order</button>
<ul>
{sortedItems.map((el, i) => (
<li key={el.id}>
<span>{el.name} </span>
<button>×</button>
</li>
))}
</ul>
</>
);
};
First of all you need to stop using useEffect for the initial state,
And if you want react to notice your changes, use an object instead of array. (This is not always that react doesn't notice your changes, but since you didn't change array and it was only sorted, react ignores it).
const ShoppingList = () => {
const [items, setItems] = useState({
data: [
{ id: 1, name: 'Soda' },
{ id: 2, name: 'ice' },
],
});
const handleSort = () => {
const sortedItems = items.data.sort((a, b) => {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
setItems({
data: sortedItems,
});
};
return (
<>
<button onClick={() => handleSort()}>Sort by name</button>
<ul>
{items.data.map((el, i) => (
<li key={el.id}>
<span>{el.name} </span>
<button onClick={() => handleRemove(item.id)}>×</button>
</li>
))}
</ul>
</>
);
}
Hope this helps 🙂
If you are interested to know more indepth on why the array items is changed (sorted) but React doesn't render, there are 2 things to take note:
How array.sort work
How React re-render with useState
For (1), it's easy, array.sort return the sorted array. Note that the array is sorted in place, and no copy is made. Hence sortItems and items still refer to the same array
For (2), it's a little bit complicated as we have to read through React code base.
This is React.useState signature
export function useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
Use Github navigation tools and scan we gonna end-up to this code:
useState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
currentHookNameInDev = 'useState';
mountHookTypesDev();
const prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
}
Next we must find the definition of mounState:
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
...
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
Notice, the return type of mountState is an array where the 2nd argument is an function just like const [items, setItems] = useState([])
Which means we almost there.
dispatch is the value from dispatchAction.bind
Scan through the code we gonna end up at this line:
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
Last part is what function is does:
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
It simply check for equality using === operator.
Comeback to your sort function, nextState in our case is sortItems and prevState is items. With (1) in mind, sortItems === items => true so React gonna skip the rendering.
That's why you see most of the tutorials states that you have to do shallow copy.
By doing so your nextState will differ from your prevState.
TLDR:
React use function is above to check for state changes when using hooks
Always make shallow copy when working with array, object if you are using hooks
Related
In my project, I need to get selected items from a Flatlist and pass them to my parent component.
I created a local state like this:
const [myState, setMyState] = useState<IStateType[] | []>([])
Each time an item is selected I try to add it to my useEffect:
useEffect(() => {
const result = myState.filter((el) => el.id !== item.id)
if (isSelected) {
setMyState([
...result,
{
propOne: 0,
propTwo: 1,
id: item.id,
...
},
])
} else {
setMyState(result)
}
}, [isSelected])
But I would need to put mySate in the dependency of my useEffect to add each time the new items selected. If I add it to the useEffect dependency it causes an infinite loop ^^
How to add each new item to my array while listening to all the changes and without causing an infinite loop?
I believe the issue you're having it's because you're not separating the concerns of each component correctly, once you have to relay on the previous data every time, the useEffect can be tricky. But there are two solutions to your issue:
Make use of useState callback function:
The useState function can be used with a callback rather than a value, as follows:
useEffect(() => {
if (isSelected) {
setMyState(prevState => [
...prevState,
{
propOne: 0,
propTwo: 1,
id: item.id,
...
},
])
} else {
setMyState(result)
}
}, [isSelected])
Best structure of your components + using useState callback function
What I could see about your approach is that you (as you showed) seems to be trying to handle the isSelected for each item and the myState in the same component, which could be done, but it's non-ideal. So I propose the creation of two components, let's say:
<List />: Should handle the callback for selecting an item and rendering them.
<List />:
function List() {
const [myState, setMyState] = useState([]);
const isItemSelected = useCallback(
(itemId) => myState.some((el) => el.id === itemId),
[myState]
);
const handleSelectItem = useCallback(
(itemId) => {
const isSelected = isItemSelected(itemId);
if (isSelected) {
setMyState((prevState) => prevState.filter((el) => el.id !== itemId));
} else {
setMyState((prevState) => prevState.concat({ id: itemId }));
}
},
[isItemSelected]
);
return (
<div>
<p>{renderTimes ?? 0}</p>
{items.map((item) => (
<Item
item={item}
onSelectItem={handleSelectItem}
selected={isItemSelected(item.id)}
/>
))}
</div>
);
}
<Item />: Should handle the isSelected field internally for each item.
<Item />:
const Item = ({ item, selected = false, onSelectItem }) => {
const [isSelected, setIsSelected] = useState(false);
useEffect(() => {
setIsSelected(selected);
}, [selected]);
return (
<div>
<p>
{item.name} is {isSelected ? "selected" : "not selected"}
</p>
<button onClick={() => onClick(item.id)}>
{isSelected ? "Remove" : "Select"} this item
</button>
</div>
);
};
Here's a codesnack where I added a function that counts the renders, so you can check the performance of your solution.
I wrote a simple component in React which is supposed to remove the last element of an array when a button is clicked, but despite console logging the correct value, my React state is not being updated accordingly (when viewing the components with React Developer Tools) and hence the output is not changing. I'm at a loss as to why it won't update correctly.
const Test = () => {
const [input, setInput] = useState(["c", "a", "t"])
const removeLastElementFromArray = () => {
setInput(prev => {
console.log("Previous array was:", prev)
let t = prev.pop();
console.log("Array popped, array is now:", prev)
return prev;
})
}
return (
<>
<button onClick={removeLastElementFromArray}>Click</button>
{input.map((char, index) => <span key={index}>{char}</span>)}
</>
)
}
See the state is not getting updated because arrays are mutable data types and when you pop an element, it is not going to change the address of the array in the memory. Since the array(address) has not changed, react will figure the state hasn't changed and that's why a re-render won't be triggered
Try this code
const App = () => {
const [input, setInput] = useState(["c", "a", "t"])
const removeLastElementFromArray = () => {
setInput(prev => {
console.log("Previous array was:", prev)
prev.pop();
let t= [...prev]
console.log("Array popped, array is now:", prev)
return t;
})
}
return (
<>
<button onClick={removeLastElementFromArray}>Click</button>
{input.map((char, index) => <span key={index}>{char}</span>)}
</>
)
}
I'm still struggling with React Natives rendering order. I'm fetching the API, then I filter this data and finally I'm manipulating the data. When I first load the app, it does not show the Data appropriately only when I'm saving within my code editor it shows up.
My simplified code:
const [data, setData] = useState([]);
const [sumPost, setSumPost] = useState(0);
const [sumProd, setSumProd] = useState(0);
useEffect(() => {
const unsubscribe = db.collection("Dates").where("projektName", "==", Projektname).onSnapshot(snapshot => (
setData(
snapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
})))
))
return unsubscribe;
}, [])
const produktionsFilter = data.filter( x =>
x.data.jobtype == "Produktion"
);
const postFilter = data.filter( x =>
x.data.jobtype == "Postproduktion"
);
const terminFilter = data.filter( x =>
x.data.jobtype == "Termin"
);
let i;
const addPostProdTage = () => {
const post = [];
const prod = [];
for(i=0; i < postFilter.length; i++){
const p = postFilter[i].data.alleTage.length;
post.push(p)
}
for(i=0; i < produktionsFilter.length; i++){
const l = produktionsFilter[i].data.alleTage.length;
prod.push(l)
}
setSumPost(post.reduce(function(a, b){
return a + b;
}, 0));
setSumProd(prod.reduce(function(a, b){
return a + b;
}, 0));
}
useEffect(() => {
addPostProdTage();
}, [])
return(
<View>
<Text>{sumPost}</Text>
<Text>{sumProd}</Text>
</View>
)
sumProd should be 18 and sumPost should be 3. Right now it is showing 0 on both, because both states are empty arrays initially. It needs to some sort refresh.
I'm sure, there are more efficient ways to code this, but I need help to understand, why my data is not showing appropriately when I first load the app, because I'm running into this problem over and over again.
Thanks to all the advise I got on here, so for future reference this is how I solved this:
I filtered the data inside snapshot:
useEffect(() => {
const post = db
.collection("Dates")
.where("projektName", "==", Projektname)
.where("jobtype", "==", "Postproduktion")
.onSnapshot((snapshot) =>
setPost(
snapshot.docs.map((doc) => ({
id: doc.id,
data: doc.data(),
}))
)
);
return post;
}, []);
I had unnecessary steps to do my calculation. I could simplify this into a single function:
const revData = () => {
setSumPost(
post.reduce(function (prev, cur) {
return prev + cur.data.alleTage.length;
}, 0)
);
};
And finally, I had a useEffect to call that function after the data has been fetched using the dependency array:
useEffect(() => {
revData();
}, [post]);
You are creating local variables that go out of scope. You would be able to catch this error if you were using typescript instead of javascript.
You want to instead create state objects like this:
const [sumPost, setSumPost] = useState(0)
const [sumProd, setSumProd] = useState(0);
And then set the values of those objects as shown:
setSumPost(postproduktionsTage.reduce(function(a, b){
return a + b;
}, 0));
setSumProd(produktionsTage.reduce(function(a, b){
return a + b;
}, 0));
And then you can use it as you desire:
return(
<View>
<Text>{sumPost}</Text>
<Text>{sumProd}</Text>
</View>
)
So i want to build a voting app thing, and I need to set the number of votes into state so that it updates on click.But I don't exactly know how to set one property of an object without disrupting the other oroperty and the other object.
import './App.css';
import React, {useState} from 'react'
function App() {
const [votes, setVotes] = useState([
{
voteNum: 0,
name: "Item 1"
},
{
voteNum: 0,
name: "Item 2"
}
])
const addVote = (vote) => {
vote.voteNum++
setVotes( /* How do I set the votes.voteNum to be equal to vote.voteNum from the line above */)
}
return (
<div className="App">
<h1>Cast Your Votes!</h1>
{votes.map(vote => (
<form className="votee" onClick={() => addVote(vote)}>
<h1>{vote.voteNum}</h1>
<h2>{vote.name}</h2>
</form>
))}
</div>
);
}
export default App;
You should update react state in immutable way:
const addVote = (vote) => {
setVotes(votes.map(x => x === vote ? ({
...x,
voteNum: x.voteNum + 1
}) : x))
}
Or better use some unique Id in comparison, e.g. x.id === vote.id if you have Ids for your objects; or as mentioned in another answer, you can also use element array index in comparison (pass that to the function instead of vote).
const addVote = (vote) => {
const _votes = JSON.parse(JSON.stringify(votes))
const index = _votes.findIndex(v => v.name === vote.name)
if (index !== -1) {
_votes[index].voteNum ++
}
setVotes(_votes)
}
Pass index to function
{votes.map((vote, index) => (
<form className="votee" onClick={() => addVote(index)}>
<h1>{vote.voteNum}</h1>
<h2>{vote.name}</h2>
</form>
))}
addVote function would look like
const addVote = (index) => {
votes[index].voteNum ++;
setVotes([...votes]);
}
Note: Usestate is prefrered to use primitive data types i.e string, boolean and number
For array and object make use of useReducer
Making a comment section for a website and I ran into a big problem. Currently I have a delete button that splices the comments from state based on their index. I need to show the array in reverse to the user--so when they make multiple comments the newest one is ontop.
The problem is if I reverse() the mapped array the index doesn't get reversed with it, so clicking delete for item 1 deletes the last item, and vice versa.
const [userComments, setUserComments] = useState([])
const postComment = (event, userComment) => {
if (event.key === 'Enter') {
setUserComments(prevState => ([...prevState, {comment: userComment}]))
}
}
const deleteComment = (e, i) => {
const userCommentsArray = [...userComments]
userCommentsArray.splice(i, 1)
setUserComments(prevState => ([...prevState], userCommentsArray))
}
return (
<input
placeholder="Add a public comment"
onKeyUp={event => postComment(event, event.currentTarget.value)}
onClick={event => showCommentButtons()}
/>
{ userComments
? userComments.map((item, i) => (
<div className="button" onClick={e => deleteComment(e, i)}>Button</div>
<p className="comment">{item.comment}</p>
))
: null
}
)
Use reverse method on array:
const deleteComment = (e, i) => {
const userCommentsArray = [...userComments].reverse()
userCommentsArray.splice(i, 1)
setUserComments(prevState => ([...prevState], userCommentsArray.reverse()))
}
Figured it out. Used unshift to push the items to state in reverse order.
No other changes necessary.
const postComment = (userComment) => {
const userCommentNotBlank = !userComment.trim().length < 1
if (userCommentNotBlank) {
const newState = [...userComments]
newState.unshift(userComment)
setUserComments(prevState => ([...prevState], newState))
resetAddComment()
}
}