What is the best way to update an object in a usestate and see the change immediately - reactjs

const TestScreen = (props) => {
const [data, setData] = useState([
{ "id": "0", "user": "Lisa","amount": 1 },
]);
return(
<View style={{
flex:1,
alignItems:'center',
justifyContent:'center'
}}>
<Text>amount : {data[0].amount}</Text>
<Text>User : {data[0].user}</Text>
<Button title="update" onPress={()=>setData(??????)}></Button>
</View>
)
}
export default TestScreen;
what is the best way to add an amount number on the user Lisa? i can do
// setData([{ "id": "0", "user": "Lisa","amount": data[0].amount + 1}])
but what i have 5 users or 20?
even with a returning function nothing gets updated exept console logged witch show me the actual value
let countAddFunc =(getArr)=>{
let arr = getArr
arr[0].amount++
console.log(arr[0].amount);
return(
arr
)
}
<Button title="update" onPress={()=>setData(countAddFunc(data))}></Button>

One of the key concepts in React is Do Not Modify State Directly. This can be tricky sometimes, especially when dealing with nested data like in your example (a number as a property of an object inside an array).
Below, I refactored your code and added comments to help explain. By creating a function to update the state, and passing that function to each child component's props, the child component will be able to update the state by calling the function.
const {useState} = React;
// component dedicated to displaying each item in the array
const Item = (props) => (
<div style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
}}>
<div>Amount: {props.item.amount}</div>
<div>User: {props.item.user}</div>
<button onClick={() => {
props.updateAmount(props.item.user);
}}>Increment</button>
</div>
);
const ItemList = () => {
const [data, setData] = useState([
{
id: '0',
user: 'Lisa',
amount: 1,
},
{
id: '1',
user: 'Walter',
amount: 3,
},
]);
const updateAmount = (user, changeAmount = 1) => {
// find the index of the item we want
const index = data.findIndex(item => item.user === user);
// return early (do nothing) if it doesn't exist
if (index === -1) return;
const item = data[index];
// don't modify state directly (item is still the same object in the state array)
const updatedItem = {
...item,
amount: item.amount + changeAmount,
};
// again, don't modify state directly: create new array
const updatedArray = [...data];
// insert updated item at the appropriate index
updatedArray[index] = updatedItem;
setData(updatedArray);
};
return (
<ul>
{data.map(item => (
<li key={item.id}>
<Item item={item} updateAmount={updateAmount} />
</li>
))}
</ul>
);
};
ReactDOM.render(<ItemList />, document.getElementById('root'));
<script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>

can you try like this once, pass second param as user id you want to update
<Button title="update" onPress={()=>setData(countAddFunc(data, 0))}></Button>
let countAddFunc =(getArr, id)=>{
const arrCopy = [...getArr]
const user = arrCopy.find(u => u.id === id )
if (user) {
user.amount++
}
return arrCopy
}
actually you are modifying the state directly, and we can not update state directly and getArr is just a refrence to the data in state, so, we created a copy of array, and modified copied array, and then we set this new array into the state, about the code throwing undefined error, add a check, if (user) user.amount++ and make sure id you send onPress={()=>setData(countAddFunc(data, 0))} actually exist

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately. setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall.
Read the official documentation for more info here
Also, alternatively you can use useRef or useEffect with dependency array if you want the callback to immediately fire and do the changes.

Related

Unable to prevent Flatlist from re-rendering (already using useCallback and memo)

I'm building a to do list app as part of a coding course, using Firebase Realtime Database and React Native with Expo.
I have no problems rendering the to do list, and in this case clicking a checkbox to indicate whether the task is prioritized or not.
However, each time I click on the checkbox to change the priority of a single task in the to do list, the entire Flatlist re-renders.
Each task object is as follows:
{id: ***, text: ***, priority: ***}
Task Component: (It consists of the text of the to do (task.text), and also a checkbox to indicate whether the task is prioritized or not). I've wrapped this component in React.memo, and the only props passed down from Todolist to Task are the individual task, but it still re-renders every time. (I left out most of the standard imports in the code snippet below)
import { CheckBox } from '#rneui/themed';
const Task = ({
item,
}) => {
console.log(item)
const { user } = useContext(AuthContext);
const onPressPriority = async () => {
await update(ref(database, `users/${user}/tasks/${item.id}`), {
priority: !item.priority,
});
};
return (
<View
style={{ flexDirection: 'row', alignItems: 'center', width: '95%' }}
>
<View
style={{ width: '90%' }}
>
<Text>{item.text}</Text>
</View>
<View
style={{ width: '10%' }}
>
<CheckBox
checked={item.priority}
checkedColor="#00a152"
iconType="material-community"
checkedIcon="checkbox-marked"
uncheckedIcon={'checkbox-blank-outline'}
onPress={onPressPriority}
/>
</View>
</View>
)}
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item !== nextProps.item) {
return true
}
return false
})
To Do List parent component: Contains the Flatlist which renders a list of the Task components. It also contains a useEffect to update the tasks state based on changes to the Firebase database, and the function (memoizedOnPressPriority) to update the task.priority value when the Checkbox in the task component is clicked. Since memoizedOnPressPriority is passed a prop to , I've tried to place it in a useCallback, but is still re-rendering all items when the checkbox is clicked. (I left out most of the standard imports in the code snippet below)
export default function Home2() {
const { user } = useContext(AuthContext);
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
const tasksCopy = [];
for (let id in todos) {
tasksCopy.push({ ...todos[id], id: id });
}
setTasks(tasksCopy);
});
} else {
setTasks([]);
}
}, [user]);
const renderItem = ({ item }) => (
<TaskTwo
item={item}
/>
);
return (
<View style={styles.container}>
<FlatList
data={tasks}
initialNumToRender={5}
windowSize={4}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
);
}
Could anyone let me know what I'm doing wrong, and how I can prevent the entire Flatlist from re-rendering each time I invoke the memoizedOnPressPriority function passed down to the Task component from the TodoList parent component? Any help is much appreciated!
The flamegraph for the render is below:
Update: I moved the prioritize function (memoizedOnPressPriority) into the Task component and removed the useCallback - so it's not being passed as a prop anymore. The re-render still happens whenever I press it.
Update 2: I added a key extractor , and also a custom equality function into the memoized task component. Still keeps rendering!
I'm not familiar with Firebase Realtime Database, but if I understand the logic correctly, the whole tasks array is updated when one item changes, and this is what is triggering the list update.
Fixing the memo function
Wrapping the Task component in memo does not work because it performs a shallow comparison of the objects. The objects change each time the data is updated because a new tasks array with new objects is created, so the references of the objects are different.
See this post for more details.
To use memo, we have to pass a custom equality check function, that returns true if the component is the same with new props, like so:
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item.id === nextProps.item.id && prevProps.item.priority === nextProps.item.priority ) {
return true;
}
return false;
})
Note that is the text is modifiable, you'll want to check that too.
Alternative solution : read data from the Task component
This solution is recommended and takes full advantage of Firebase Realtime Database.
To update only the component that is updated, you need to pass an array of ids to your flatlist, and delegate the data reading to the child component.
It's a pattern I use with redux very often when I want to update a component without updating the whole flatlist.
I checked the documentation of Firebase Realtime Database, and they indeed encourage you to read data at the lowest level. If you have a large list with many properties, it's not performant to receive the whole list when only one item is updated. Under the hood, the front-end library manages the cache system automatically.
//TodoList parent Component
...
const [tasksIds, setTasksIds] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
// Build an array of ids
const tasksIdsFromDb = todos.map((todo) => todo.id);
setTasksIds(tasksCopy);
});
} else {
setTasksIds([]);
}
}, [user]);
...
// keep the rest of the code and pass tasksIds instead of tasks to the flatlist
const Task = ({ taskId, memoizedOnPressPriority }) => {
const [task, setTask] = useState(null)
const { user } = useContext(AuthContext);
useEffect(() => {
if (user) {
// retrieve data by id so only the updated component will rerender
// I guess this will be something like this
return onValue(ref(database, `users/${user}/tasks/${taskId}`), (snapshot) => {
const todo = snapshot.val();
setTask(todo);
});
} else {
setTask(null);
}
}, [user]);
if (task === null) {
return null
}
// return the component like before

React functional component to accept rendering function

I would like to find out how to write a functional component that would accept an array of objects as prop (dataSource), then render all the array objects based on a RenderFunction which I would pass as another prop. I want the render function to accept a parameter that would represent an object from the array.
Check this code snippet and the comments:
// This is my data
var dummyData = [{
"id": 1,
"name": "item1"
},
{
"id": 2,
"name": "item2"
},
{
"id": 3,
"name": "item3"
},
{
"id": 4,
"name": "item4"
},
{
"id": 5,
"name": "item5"
},
{
"id": 6,
"name": "item6"
}
]
// Functional Component :
function ListComponent(props) {
return (
<div className={"ListContainer"}>
{props.dataSource.map((x) => {
return (
<div key={x.id} className="ListItemContainer">
{props.itemRender}{" "}
</div>
);
})}{" "}
</div>
);
}
// This is my render function which I'm passing as a prop.
// I would like it to accept an argument which would become
// an object of dummy data array.
// For example if I passed x to it, the x inside the function on the first
// .map iteration would become {"id": 1,"name": "item1"}
function itemRenderTest() {
return (
<p> This is item </p>
// I want to be able to render it like this
// <p> This is {x.name} </p>
)
}
// passing props and rendering
ReactDOM.render(
<ListComponent
dataSource={dummyData}
itemRender={itemRenderTest()}
/>,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
The snippet illustrates some of the desired functionality. I can write a render function, but don't know how can I change the code so the render function could accept a parameter which would represent an object from the array.
I want to be able to write the render function like this:
function itemRenderTest(x){
return(
<p>This is {x.name}</p>
)
}
The Component would receive 2 props. The first - dataSrouce would specify the JSON array. The second - render function would define how the child list components are being rendered
<ListComponent
dataSource={dummyData}
itemRender={itemRenderTest}
/>
I'm trying to recreate a reusable component similar to what a lot of DevExtreme react components do. They basically just accept a render function definition like this renderItem={renderItemFunction} and it just works. I want to write my List component so it does the same This is a good example of one of their components
Is this possible with React? Code snippets would be really helpful.
That's 100% possible in React and a super common pattern. If I understand your question correctly -- what I typically do in this situation is
Define a parent component for the list items. It will handle fetching or otherwise retrieving the array of objects data, the overall state of that data, and the render logic for the individual list components
ListItem component, which is stateless (pure component) and simply renders reusable components based on data passed in as props. That's how component libraries create reusable components, like the one you mentioned
const ItemsList = () => {
// state variable which will store the list data
const [listData, setListData] = useState([])
// let's assume the list items are fetched from an API on initial render
useEffect(() => {
// fetch logic for list items, then update state variable
// with resulting data
const listItems = axios("https://yourapi.com/listitems")
.then(({ data }) => setListData(data)
.catch(err => console.info("Fetch error", err))
)
}, [])
const renderList = useMemo(() => listData.map(
(listItemData) => <ListItem data={listItemData}/>),
[listData])
return (
<div>
{renderList}
</div>
)
}
const ListItem = (props) => {
const { data } = props;
return (
// populate the component with the values from the data object
)
}
A few things to point out here:
useEffect hook with [] dependency will only run once on first render, retrieve the necessary data, and update the state variable initialized with the useState hook. This can be done in other ways too
useMemo hook allows us to define the render logic for the individual list components, and memoize the evaluated result. That way, this function won't run on every render, unless the value of the listData variable changes. The function provided to the useMemo hook iterates through the array of objects, and renders a ListItem components with the respective data
The ListItem component then simply receives the data as a prop and renders it
Edit based on the updated answer:
I haven't tested it but this approach should work.
const ItemsList = (props) => {
const { data, itemRender: ItemRender } = props;
const renderItems = () => data.map((itemData) => <ItemRender data={itemData}/>)
return (
<div>
{renderItems()}
</div>
)
}
const ListItem = (props) => {
const { data } = props;
return (
// populate the component with the values from the data object
)
}
const App = () => {
const data = retrieveData()
return (
<ItemsList data={data} itemRender={ListItem}/>
)
}
App component retrieves the data, decides on the component that it will use to render the individual item (ListItem) and passes both of those as props to ItemsList
ItemsList then simply maps the data to each individual component
Edit 2: basic working snippet
const ItemsList = (props) => {
const { data, itemRender: ItemRender } = props;
const renderItems = () => data.map((itemData, i) => <ItemRender data={itemData.val} key={i}/>)
return (
<div>
{renderItems()}
</div>
)
}
const ListItem = (props) => {
const { data } = props;
console.info(data)
return (
<div
style={{
width: 100,
height: 100,
border: "2px solid green",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
{data}
</div>
)
}
const App = () => {
const data = [{val: 1}, {val: 2}, {val: 3}]
return (
<div
style={{
width: "100vw",
height: "100vh",
backgroundColor: "white",
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<ItemsList data={data} itemRender={ListItem}/>
</div>
)
}
export default App;

React state not updating when used outside hook

I'm playing around with a hook that can store some deleted values. No matter what I've tried, I can't get the state from this hook to update when I use it in a component.
const useDeleteRecords = () => {
const [deletedRecords, setDeletedRecords] = React.useState<
Record[]
>([]);
const [deletedRecordIds, setDeletedRecordIds] = React.useState<string[]>([]);
// ^ this second state is largely useless – I could just use `.filter()`
// but I was experimenting to see if I could get either to work.
React.useEffect(() => {
console.log('records changed', deletedRecords);
// this works correctly, the deletedRecords array has a new item
// in it each time the button is clicked
setDeletedRecordIds(deletedRecords.map((record) => record.id));
}, [deletedRecords]);
const deleteRecord = (record: Record) => {
console.log(`should delete record ${record.id}`);
// This works correctly - firing every time the button is clicked
setDeletedRecords(prev => [...prev, record]);
};
const wasDeleted = (record: Record) => {
// This never works – deletedRecordIds is always [] when I call this outside the hook
return deletedRecordIds.some((r) => r === record.id);
};
return {
deletedRecordIds,
deleteRecord,
wasDeleted,
} // as const <-- no change
}
Using it in a component:
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecordIds, wasDeleted, deleteRecord } = useDeleteRecords();
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
}
React.useEffect(() => {
console.log('should fire when deletedRecordIds changes', deletedRecordIds);
// Only fires once for each row on load? deletedRecordIds never changes
// I can rip out the Ids state and do it just with deletedRecords, and the same thing happens
}, [deletedRecordIds]);
}
If it helps, these are in the same file – I'm not sure if there's some magic to exporting a hook in a dedicated module? I also tried as const in the return of the hook but no change.
Here's an MCVE of what's going on: https://codesandbox.io/s/tender-glade-px631y?file=/src/App.tsx
Here's also the simpler version of the problem where I only have one state variable. The deletedRecords state never mutates when I use the hook in the parent component: https://codesandbox.io/s/magical-newton-wnhxrw?file=/src/App.tsx
problem
In your App (code sandbox) you call useDeleteRecords, then for each record you create a DisplayRecord component. So far so good.
function App() {
const { wasDeleted } = useDeleteRecords(); // ✅
console.log("wtf");
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord record={record} /> // ✅
</div>
) : null;
})}
</div>
);
}
Then for each DisplayRecord you call useDeleteRecords. This maintains a separate state array for each component ⚠️
const DisplayRecord = ({ record }: { record: Record }) => {
const { deletedRecords, deleteRecord } = useDeleteRecords(); // ⚠️
const handleDelete = () => {
// called by a button on a row
deleteRecord(record);
};
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
// Only fires once for each row on load? deletedRecords never changes
}, [deletedRecords]);
return (
<div>
<div>{record.id}</div>
<div onClick={handleDelete} style={{ cursor: "pointer" }}>
[Del]
</div>
</div>
);
};
solution
The solution is to maintain a single source of truth, keeping handleDelete and deletedRecords in the shared common ancestor, App. These can be passed down as props to the dependent components.
function App() {
const { deletedRecords, deleteRecord, wasDeleted } = useDeleteRecords(); // 👍🏽
const handleDelete = (record) => (event) { // 👍🏽 delete handler
deleteRecord(record);
};
return (
<div className="App" style={{ width: "70vw" }}>
{records.map((record) => {
console.log("was deleted", wasDeleted(record));
return !wasDeleted(record) ? (
<div key={record.id}>
<DisplayRecord
record={record}
deletedRecords={deletedRecords} // 👍🏽 pass prop
handleDelete={handleDelete} // 👍🏽 pass prop
/>
</div>
) : null;
})}
</div>
);
}
Now DisplayRecord can read state from its parent. It does not have local state and does not need to call useDeleteRecords on its own.
const DisplayRecord = ({ record, deletedRecords, handleDelete }) => {
React.useEffect(() => {
console.log("should fire when deletedRecords changes", deletedRecords);
}, [deletedRecords]); // ✅ passed from parent
return (
<div>
<div>{record.id}</div>
<div
onClick={handleDelete(record)} // ✅ passed from parent
style={{ cursor: "pointer" }}
children="[Del]"
/>
</div>
);
};
code demo
I would suggest a name like useList or useSet instead of useDeleteRecord. It's more generic, offers the same functionality, but is reusable in more places.
Here's a minimal, verifiable example. I named the delete function del because delete is a reserved word. Run the code below and click the ❌ to delete some items.
function App({ items = [] }) {
const [deleted, del, wasDeleted] = useSet([])
React.useEffect(_ => {
console.log("an item was deleted", deleted)
}, [deleted])
return <div>
{items.map((item, key) =>
<div className="item" key={key} data-deleted={wasDeleted(item)}>
{item} <button onClick={_ => del(item)} children="❌" />
</div>
)}
</div>
}
function useSet(iterable = []) {
const [state, setState] = React.useState(new Set(...iterable))
return [
Array.from(state), // members
newItem => setState(s => (new Set(s)).add(newItem)), // addMember
item => state.has(item) // isMember
]
}
ReactDOM.render(
<App items={["apple", "orange", "pear", "banana"]}/>,
document.querySelector("#app")
)
div.item { display: inline-block; border: 1px solid dodgerblue; padding: 0.25rem; margin: 0.25rem; }
[data-deleted="true"] { opacity: 0.3; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Since you are updating deletedRecordIds inside a React.useEffect, this variable will have the correct value only after the render complete. wasDeleted is a closure that capture the value of deletedRecordIds when the component renders, thus it always have a stale value. As yourself are suggesting, the correct way to do that is to use .filter() and remove the second state.
Talking about the example you provided in both cases you are defining 5 hooks: one hook for each DisplayRecord component and one for the App. Each hook define is own states, thus there are 5 deletedRecords arrays on the page. Clicking on Del, only the array inside that specific component will be updated. All other component won't be notified by the update, because the state change is internal to that specific row. The hook state in App will never change because no one is calling its own deleteRecord function.
You could solve that problem in 2 way:
Pulling up the state: The hook is called just once in the App component and the deleteRecord method is passed as parameter to every DisplayRecord component. I updated your CodeSandbox example.
Use a context: Context allows many component to share the same state.

How can I modify the first object in a React UseState array?

SANDBOX LINK
I am using a FormControl to try to update my state which defaults at const [numberOfChildren, updateNumberOfChildren] = useState([{age: undefined}]); I want to modify the first object in the array when a user clicks a button, and then enters a value in an input. I try to update the age with another useState Currently, the array's first object is {age: undefined} and is not updated by the useState hook
The code looks like this
<FormControl
placeholder="Age"
aria-label="Age"
aria-describedby="basic-addon2"
onChange={async (e) => {
await updateAge(e.target.value);
}}
/>
updated by the button
<Button
className="align-button"
onClick={async (e) => {
e.preventDefault();
if(numberOfChildren.length < 1) {
await updateNumberOfChildren((children) => [
...children
]);
} else {
await updateNumberOfChildren((children) => [
...children,
{ childAge: age },
]);
}
console.log(numberOfChildren)
}}
style={{ width: "100%" }}
variant="outline-primary"
type="submit"
size="lg"
>
Add Child
</Button>{" "}
Here is a sandbox, please have a look at the console for the output SANDBOX
The way you were doing it using const [age, updateAge] = useState(undefined); won't get you what you want because by doing that you'll only update the latest added child so you can't go back to the first one and modify or even modify any previous children after adding them as the current setup has no way to differentiate between which one you're trying to modify.
So, The idea here is that you need to identify each object in the array with something unique so I changed the object structure to the following:
const [numberOfChildren, updateNumberOfChildren] = useState([
{ id: 1, age: undefined }
]);
And here's how you update the state explaining every line:
// Update numberOfChildren state
function updateData(e) {
// Grab the id of the input element and the typed value
const { id, value } = e.target;
// Find the item in the array that has the same id
// Convert the grabed id from string to Number
const itemIndex = numberOfChildren.findIndex(
item => item.id === Number(id)
);
// If the itemIndex is -1 that means it doesn't exist in the array
if (itemIndex !== -1) {
// Make a copy of the state
const children = [...numberOfChildren];
// The child item
const child = children[itemIndex];
// Update the child's age
children.splice(itemIndex, 1, { ...child, age: value });
// Update the state
updateNumberOfChildren(children);
}
}
And when you add a new child the latest added child will have the id of the numberOfChildren state length plus 1 as I used 1 as a starting point:
onClick={e => {
e.preventDefault();
updateNumberOfChildren([
...numberOfChildren,
{ id: numberOfChildren.length + 1, age: undefined }
]);
}}
Finally, If you want to check any state value don't use console.log() after setState() because setState() is async so you won't get the changes immediately and since you're using hooks the only way around this is useEffect():
// Check the values of numberOfChildren whenever they get updated
useEffect(() => {
console.log("numberOfChildren", numberOfChildren);
}, [numberOfChildren]);
Here's the sandbox. Hopefully everything now is crystal clear.

Does useState hook change the value of the state

I just started React, and in this Item list tutorial I have some question about updating the states of the item. Also, I'm using functional component .So in app.js
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false
},
...
])
Then I have a function in app.js to update the bought to true or false when I check a box
// The id is passed in from Item.js down below
const markBought = (id) => {
setItems(
items.map(
item => {
if (item.id === id) {
/// If bought is false, checking it will make it true and vice versa
item.bought = !item.bought; // (1)
}
return item; // (2)
})
);
};
return (
<div className="App">
<Items items={items} markBought={markBought}></Items>
</div>
);
The teacher said we are using something called Component Drilling. So in Items.js, we map through every item to display them one by one, but I don't think it is neccessary to show.
Finally in Item.js
<input type="checkbox" onChange={() => props.markBought(props.item.id)} />
{props.item.title}
The application worked perfectly, but it's a little bit confusing for me. So:
In app.js, after we change the bought status, shouldn't we also need to return item, the same way we return the item if the condition is false? Why only return the item when if is wrong, but when it is right we only change it without a return?
I read that map will not modify the array, so markBought function should create a new items array, with the bought modified already, but what happens to this array, how do React know to "props" this to item.js, rather than the ones I hard coded?
Sorry if this is a little bit long, any help will be really appreciated. Thanks for reading
You are mutating an item in your map, if you optimized your Item component to be a pure component then that component won't re render because of the mutation. Try the following instead:
//use useCallback so marBought doesn't change and cause
// needless DOM re renders
const markBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? { ...item, bought: !item.bought } //copy item with changed value
: item //not this item, just return the item
)
);
}, []);
Here is a full example:
const { useCallback, useState, useRef, memo } = React;
function Items() {
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false,
},
{
id: 2,
title: 'Peach',
bought: false,
},
]);
const toggleBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? { ...item, bought: !item.bought } //copy item with changed value
: item //not this item, just return the item
)
);
}, []);
return (
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
toggleBought={toggleBought}
/>
))}
</div>
);
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
const renderedRef = useRef(0);
renderedRef.current++;
return (
<div>
<div>{item.title}</div>
<div>bought: {item.bought ? 'yes' : 'no'}</div>
<button onClick={() => toggleBought(item.id)}>
toggle bought
</button>
<div>Rendered: {renderedRef.current} times</div>
</div>
);
});
//render the application
ReactDOM.render(<Items />, 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>
Here is a broken example where you mutate the item and won't see the re render even though the state did change:
const { useCallback, useState, useRef, memo } = React;
function Items() {
const [items, setItems] = useState([
{
id: 1,
title: 'Banana',
bought: false,
},
{
id: 2,
title: 'Peach',
bought: false,
},
]);
const toggleBought = useCallback(id => {
setItems((
items //pass callback to the setter from useState
) =>
items.map(
item =>
item.id === id
? ((item.bought = !item.bought),item) //mutate item
: item //not this item, just return the item
)
);
}, []);
return (
<div>
<div>
{items.map(item => (
<Item
key={item.id}
item={item}
toggleBought={toggleBought}
/>
))}
</div>
<div>{JSON.stringify(items)}</div>
</div>
);
}
//use memo to make Item a pure component
const Item = memo(function Item({ item, toggleBought }) {
const renderedRef = useRef(0);
renderedRef.current++;
return (
<div>
<div>{item.title}</div>
<div>bought: {item.bought ? 'yes' : 'no'}</div>
<button onClick={() => toggleBought(item.id)}>
toggle bought
</button>
<div>Rendered: {renderedRef.current} times</div>
</div>
);
});
//render the application
ReactDOM.render(<Items />, 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>
Hi there and welcome to Stackoverflow.
You are always returning the item. You just have an if statement that will change the bought state and item will get returned even if the condition above was false, which is the correct way of doing.
Map will indeed not modify the array but return a new one. If you want to get that returning array you could simply do :
const myNewArray = items.map(...)
The way this new array is getting to your other component is because this new array is given to your useState().
You see setItems() ? This will set your state and Item.js will automatically be updated.
That is what is so great about react. All components that are served from state will be updated once this state is updated.
Your map function always returns an item. It just modifies the item first if the item you're modifying matches the id of the item currently being mapped. Map returns a new array of items (even if it doesn't change anything), which causes useState to see a new value. By default, in React, the check for updates isn't very clever - it's just checking if oldValue === newValue.
For primitives like strings, object equality tests return true for two separate objects, as long as their values match.
"foo" === "foo" // => true
However, this isn't true for object or arrays. Two different arrays containing the same values will not compare as equal (because Javascript isn't comparing their contents, but rather their object IDs):
["foo"] === ["foo"] // => false
So, when you map your items, you get a new array object (because recall: map collects the return values of the callback function into a new array), which will never match the previous value of items, so every call to setItems will cause React to say "hm, my previous items isn't the same object as my new items, I must re-render this component".

Resources