New to React. I am trying to figure out how to set the state values of all objects with the values of a one specific object.
Here is the CodeSandbox: https://codesandbox.io/s/blissful-https-1o415
If you click on the Apply to All button in the Fork section, the data in Spoon and Knife would be updated to be the same as that in the Fork section. Is it possible to set the data of one object in the map, with that of another object? Would it happen in the map function or somewhere else? Thanks in advance for any help.
import React, { useState, useEffect } from "react";
import axios from "axios";
import "./styles.css";
function App() {
const [kitchenItems, setkitchenItems] = useState([]);
useEffect(() => {
axios
.get("./data.json")
.then((response) => setkitchenItems(response.data.kitchen));
}, []);
function applyAll(i) {
const updatedData = kitchenItems.map((item, idx) => {
if (idx === i) {
return {
...item,
size: item.size,
text: item.text,
value: item.value
};
}
return item;
});
setkitchenItems(updatedData);
console.log(kitchenItems);
}
return (
<main>
{kitchenItems.map((item, i) => (
<div className="utensil" key={i}>
<h2>{item.name}</h2>
<ul>
<li>{item.size}</li>
<li>{item.text}</li>
<li>{item.value}</li>
</ul>
<button type="button" name="apply" onClick={() => applyAll(i)}>
Apply to All
</button>
</div>
))}
</main>
);
}
export default App;
As stated in another answer, you will not see the update with the console log, since it would be an asynchronous and you would need to use a useEffect to see the update. You are close on your code though. Couple things:
You need to use a stable function for the applyAll (useCallback hook)
You were looking at index equal to the object you are trying to copy, and just using it's same values. This only changed spoon into an already spoon for it's values of size and more. What you wanted was to change the others. Here is an example I made on your codepen:
import React, { useCallback, useEffect, useState } from "react";
import axios from "axios";
import "./styles.css";
function App() {
const [kitchenItems, setkitchenItems] = useState([]);
useEffect(() => {
axios
.get("./data.json")
.then((response) => setkitchenItems(response.data.kitchen));
}, []);
const applyAll = useCallback((i) => {
const updatedData = kitchenItems.map((item, idx) => {
if (idx !== i) { //<-- You want to check if the index of the item doesn't equal the index of the item you want to change it to.
return {
...item,
size: kitchenItems[i].size, //<-- Since you have the index of the item, you can just call to get it's values here from kitchenItems
text: kitchenItems[i].text,
value: kitchenItems[i].value
};
}
return item;
});
setkitchenItems(updatedData);
}, [kitchenItems]);
return (
<main>
{kitchenItems.map((item, i) => (
<div className="utensil" key={i}>
<h2>{item.name}</h2>
<ul>
<li>{item.size}</li>
<li>{item.text}</li>
<li>{item.value}</li>
</ul>
<button type="button" name="apply" onClick={() => applyAll(i)}>
Apply to All
</button>
</div>
))}
</main>
);
}
export default App;
If I understand what you are trying to accomplish, this should solve your issue.
setkitchenItems() is the asynchronous method, and you can't get the updated value of kitchenItems immediately after setkitchenItems().
setkitchenItems(updatedData);
console.log(kitchenItems); // This will console the old `kitchenItems`
You should use useEffect with adding kitchenItems dependency to check updated state value.
useEffect(() => {
console.log(kitchenItems);
}, [kitchenItems]);
Related
I am new to React and learning about states and props.
I am following a React Wes Bos course and the teacher is using class components, so I am sort of refactoring as I go along to functional component (for exercise and because I have to learn those).
We are coding an app that is supposed to be a fish restaurant, and at some point, we want to load to the order section some values.
I have two main problems:
1 - When I try to run the method addToOrder(key) manually in the React dev tool by using $r on App.js, I get an error
VM761:1 Uncaught TypeError: $r.addToOrder is not a function
2 - The second issue is that when I click on the button Add To Order, the one that is supposed to update the order{} object, the order object itself does not get updated.
I have been searching for a good half day now and I am not sure what could be wrong.
As a self-check:
the prop index is passed correctly from to as I can console.log(index) and do get the current one.
I am sorry if I am not explaining myself properly, it's a bit hard to condense into a short post. Do ask questions and clarifications as needed, I'll do my best to provide the correct info.
Here's the two components code:
App
import React from "react";
import { Header } from "./Header";
import { Order } from "./Order";
import { Inventory } from "./Inventory";
import { useState } from "react";
import sampleFishes from "../sample-fishes";
import { Fish } from "./Fish";
export const App = () => {
const [state, setState] = useState({
fishes: {},
order: {},
});
/**
* Structure of the function served in <AddFishForm>
* Making a copy of the state to avoid mutations ...state.fishes
* Date.now() used to assign a unique key
*
*/
const addFish = (fish) => {
const fishes = { ...state.fishes };
fishes[`fish${Date.now()}`] = fish;
setState({
fishes: fishes,
});
};
/**
* Function to display a sample fishes in the list
* Made to avoid manual typing
* Fish data comes from ../sample-fishes
*/
const loadSampleFishes = () => {
setState({ fishes: sampleFishes });
};
/**
* Take a copy of state
* Either add to the order or update the number in order
* (if order exists, adds one to it, if not, set it to one)
* Call setState() to update state object
*/
const addToOrder = (key) => {
const order = { ...state.order };
order[key] = order[key] + 1 || 1;
setState({
order: order,
});
};
return (
<div className="catch-of-the-day">
<div className="menu">
<Header tagline="Fresh Seafood Market" />
<ul className="fishes">
{Object.keys(state.fishes).map((key) => {
return (
<Fish
key={key}
details={state.fishes[key]}
addToOrder={addToOrder}
></Fish>
);
})}
</ul>
</div>
<Order />
<Inventory addFish={addFish} loadSampleFishes={loadSampleFishes} />
</div>
);
};
Fish
import React from "react";
import { formatPrice } from "../helpers";
export const Fish = ({ details, addToOrder, index }) => {
const isAvailable = details.status === "available";
const handleClick = () => {
addToOrder[index];
};
return (
<li className="menu-fish">
<img src={details.image} alt="" />
<h3 className="fish-names">
{details.name}
<span className="price">{formatPrice(details.price)}</span>
</h3>
<p>{details.desc}</p>
<button type="submit" disabled={!isAvailable} onClick={() => handleClick}>
{isAvailable ? "Add to order" : "Sold out!"}
</button>
</li>
);
};
calling the function from $r
TL;DR Solution
Now that I know what I am looking for, this was the issue: updating and merging an object using React useState hook.
I was missing to copy the previous state when updating order{}
The rest was pretty much correct, so the bit of code with the improvement is:
const addOrder = (key) => {
const order = { ...state.order };
order[key] = order[key] + 1 || 1;
setState({
...state,
order: order,
});
};
This post (as well as the last answer on this one) really explains it well: https://stackoverflow.com/a/61243124/20615843
This is the relative bit in the React docs:https://reactjs.org/docs/hooks-reference.html#functional-updates
Apparently, and even better practice is using useReducer() as stated: https://stackoverflow.com/a/71093607/20615843
The onClick={() => handleClick} should be
either with parenthesis at the end to call it
onClick={() => handleClick()}
or better yet, pass it directly as the callback method
onClick={handleClick}
First welcome to react world Jim Halpert! Hope you are enjoying your journey.
Their a couple of issues I have found in your example.
1)In the Fish.jsx click handler you need to pass the index while calling the actual function.
2)You have to bind the index as props, from the parent JSX file.
3)Since you have order and fishes in the same object you will have to copy the previous data as well as shown in line 57 of App.jsx
I have tweaked your example a bit, have a look at it below:
import React from "react";
import { useState } from "react";
import { Fish } from "./Fish";
const App = () => {
const [state, setState] = useState({
fishes: {
"salmon" : {
"image":"https://via.placeholder.com/20x20",
"name":"salmon",
"description":"test",
"status":"available"
}
},
order: {},
});
/**
* Structure of the function served in <AddFishForm>
* Making a copy of the state to avoid mutations ...state.fishes
* Date.now() used to assign a unique key
*
*/
const addFish = (fish) => {
const fishes = { ...state.fishes };
fishes[`fish${Date.now()}`] = fish;
setState({
fishes: fishes,
});
};
/**
* Function to display a sample fishes in the list
* Made to avoid manual typing
* Fish data comes from ../sample-fishes
*/
const loadSampleFishes = () => {
setState({ fishes: sampleFishes });
};
/**
* Take a copy of state
* Either add to the order or update the number in order
* (if order exists, adds one to it, if not, set it to one)
* Call setState() to update state object
*/
const addToOrder = (fish) => {
const order = { ...state.order };
order[fish] = order[fish.key] + 1 ?? 1;
console.log("Order >>>>>>>>>>>> "+JSON.stringify(order));
setState({
...state,
order: order,
});
};
return (
<div className="catch-of-the-day">
<div className="menu">
<ul className="fishes">
{Object.keys(state.fishes).map((key) => {
return (
<Fish
index={key}
details={state.fishes[key]}
addToOrder={addToOrder}
></Fish>
);
})}
</ul>
</div>
</div>
);
};
export default App;
import React from "react";
export const Fish = ({ details, addToOrder, index }) => {
const isAvailable = details.status === "available";
const handleClick = (index) => {
addToOrder(index);
};
return (
<li className="menu-fish">
<img src={details.image} alt="" />
<h3 className="fish-names">
{details.name}
<span className="price">{details.price}</span>
</h3>
<p>{details.desc}</p>
<button type="submit" disabled={!isAvailable} onClick={() => handleClick(index)}>
{isAvailable ? "Add to order" : "Sold out!"}
</button>
</li>
);
};
CodeSandbox - https://codesandbox.io/s/distracted-taussig-n7e7q3?file=/src/App.js
As you can see, I iterate over the flaggers array with .map and render <div>true</div> or <div onClick={() => setToTrue(flag)}>false</div>. I assumed that if I were to click the second div, the refer property of that flag would be set to true and the component would re-render, making the div change to <div>true</div> but that doesn't seem to be the case.
In the setToTrue function I console.log the post object, and I can see that the refer property of the second flag has changed to true, but it is not shown in the UI.
import "./styles.css";
export default function App() {
const post = {
flaggers: [
{
refer: false
},
{
refer: false
}
]
}
const setToTrue = (flag) => {
flag.refer = true;
console.log(post)
}
return (
<div className="App">
{post.flaggers.map((flag) => (
<div>
{flag.refer ? <div>true</div> : <div onClick={() => setToTrue(flag)}>false</div>}
</div>
))}
</div>
);
}
Well, that's not how react you have to setState value to trigger the component to rerender which will cause to UI change try the below code it will work fine as I set a value on onClick that causes the component to rerender in short. I would suggest reading the documentation before going into the coding and know-how react works
React Documentation
export default function App() {
const [post, setPost] = React.useState([
{
refer: false,
},
{
refer: false,
},
]);
const setToTrue = (boolFlag, index) => {
const tempPost = [...post];
tempPost[index].refer = boolFlag;
setPost(tempPost);
};
return (
<div className='App'>
{post.map((flag, index) => (
<div key={`${index}flag`}>
{flag.refer ? <div onClick={() => setToTrue(false, index)}>true</div> : <div onClick={() => setToTrue(true, index)}>false</div>}
</div>
))}
</div>
);
}
Can you help me figure out why I am not able to map this array. Here below are the error and the codes I am running:
TypeError: posts.map is not a function
and here is my codes causing the above error:
import React from 'react';
import {useEffect, useState} from 'react';
import { Container, Row, Col } from 'bootstrap-4-react';
export default function Post() {
//posts array to be mapped
const [posts, setPosts] = useState([{
title: "",
postContent: ""
}]);
useEffect(() => {
//fetches a GET route
fetch(`${process.env.REACT_APP_SERVER_URL}/posts/:id`).then(res => {
if (res.ok) {
return res.json()
}
}).then(jsonRes => setPosts(jsonRes));
})
return (
<div>
<h1>Hello</h1>
//cant seem to be able to map this array
{posts.map(post => {
<>
<h1>{post.title}</h1>
<p>{post.postContent}</p>
</>
})}
</div>
)}
You need to wrap the mapped returned code block within parenthesis ()
and not in curly brackets {} in order to return the html correctly
//...
return (
<div>
<h1>Hello</h1>
{posts.map(post => (
<>
<h1>{post.title}</h1>
<p>{post.postContent}</p>
</>
))}
</div>
)
}
Edit:
Also, I suggest adding an empty dependency array as the second argument for your useEffect(() => { //your code }, [])
This will make it so your component doesn't re-render sporadically and end up fetching your data a ridiculous amount of times.
This is maybe because the response is not an array. Try to console.log the response. You can also change its type by using Array.isArray(jsonRes). The second problem is you are not returning the individual element inside the map function.
{posts.map((post) => (
<>
<h1>{post.title}</h1>
<p>{post.postContent}</p>
</>
))}
Your useEffect also don't have any dependencies, this will result in fetching data on every render. So you must use an empty array as the second argument inside useEffect to tell it to execute only once.
useEffect(() => {
//fetches a GET route
fetch(`${process.env.REACT_APP_SERVER_URL}/posts/:id`).then(res => {
if (res.ok) {
return res.json()
}
}).then(jsonRes => setPosts(jsonRes));
}, [])
setState() doesn't work on first click ! the state value gets updated only on second , third ....clicks. i used proper contexts and imports to handle the states!
I'll quickly summarize what im doing top nav bar has two buttons , home and cart.
Side nav bar has three hero buttons, on click renders the respective hero store which has tshirts , socks and shoes with + and - buttons to increase or decrease the quantity.
on each click the value of span that displays the quantity increases correctly but the cart buttons shows the quantity excluding the first clicks. Like when i increment the tshirts value to 1 , the cart button doesn't show any value ,as i increment the tshirts value to 2 the cart button shows 1
cartButton uses the state CartValue
tshirts,socks,shoes use the state HeroGoods
(live demo) click here to see the what im talking about
i'm not sure if im allowed to post all the components and external links like github here. but anyways if you guys cant see where i went wrong from the code below , here's link to the github repo
import React , {useState,useEffect}from 'react';
import Navmenu from './Navmenu'
import SideNav from './SideNav'
import ActionDiv from './ActionDiv'
import ActionHeroStore from './ActionHeroStore'
import ActionCart from './ActionCart'
import '../css/main.css'
export const HeroContext=React.createContext()
const emptyGood={
tshirts:0,
shoes:0,
socks:0,
}
const emptyCart={
batman:{
tshirts:0,
shoes:0,
socks:0,
},
superman:{
tshirts:0,
shoes:0,
socks:0,
},
greenlantern:{
tshirts:0,
shoes:0,
socks:0,
},
}
function empty()
{
return null
}
function App() {
const [hero,setHero]=useState(null)
const [cartValue,setCartValue]=useState(emptyCart)
const [batmanGoods,setBatmanGoods]=useState(emptyGood)
const [supermanGoods,setSupermanGoods]=useState(emptyGood)
const [greenLanternGoods,setGreenLanternGoods]=useState(emptyGood)
const [showCart,setShowCart]=useState(false)
function handleUpdateGoods(hero,obj){
hero=='batman'?
setBatmanGoods(prevState=>{
return {...prevState,...obj}
}):
hero=='superman'?
setSupermanGoods(prevState=>{
return {...prevState,...obj}
}):
hero=='greenlantern'?
setGreenLanternGoods(prevState=>{
return {...prevState,...obj}
}):
empty()
}
function handleHeroSelect(name){
setHero(prevState=>prevState=name)
}
function handleCartValue(value)
{
setCartValue(value)
}
function handleShowCart(status)
{
setShowCart(status)
}
function giveHeroGoods(hero,element)
{
return (
hero=='batman'?batmanGoods[element]:
hero=='superman'?supermanGoods[element]:
hero=='greenlantern'?greenLanternGoods[element]:empty()
)
}
function handleUpdateCart(name){
name=='batman'?
setCartValue(prevState=>{
return {...prevState,batman:{...batmanGoods}}
}):
name=='superman'?
setCartValue(prevState=>{
return {...prevState,superman:{...supermanGoods}}
}):
name=='greenlantern'?
setCartValue(prevState=>{
return {...prevState,greenlantern:{...greenLanternGoods}}
}):
empty()
}
const heroContextValue={
handleHeroSelect,
handleCartValue,
handleUpdateGoods,
giveHeroGoods,
handleUpdateCart,
handleShowCart
}
return (
<>
<HeroContext.Provider value={heroContextValue}>
<Navmenu cartValue={cartValue}/>
<div className="mainContent">
<SideNav cartValue={cartValue}/>
{hero==null && !showCart &&<ActionDiv/>}
{hero!==null && !showCart && <ActionHeroStore hero={hero}/>}
{showCart && <ActionCart cartValue={cartValue}/>}
</div>
</HeroContext.Provider>
</>
)
}
export default App;
import React ,{useContext} from 'react'
import {HeroContext} from './App'
export default function Navmenu(props) {
const {cartValue}=props
const {handleHeroSelect,handleShowCart}=useContext(HeroContext)
function giveGoodsSum(obj)
{
return obj.tshirts+obj.socks+obj.shoes
}
function giveCartValue(cartValue){
let sum=0
for(let key in cartValue)
{
sum=sum+giveGoodsSum(cartValue[key])
}
return(
sum!==0?sum:null
)
}
return (
<div
className="navMenu"
>
<button
className="homeButton"
onClick={()=>{
handleHeroSelect(null)
handleShowCart(false)
}}
>
Home
</button>
<button
className="cartButton"
onClick={()=>{
handleHeroSelect(null)
handleShowCart(true)
}}
>
cart
<span
>
{giveCartValue(cartValue)}
</span>
</button>
</div>
)
}
import React ,{useContext} from 'react'
import {HeroContext} from './App'
export default function SideNav() {
const {handleHeroSelect}=useContext(HeroContext)
return (
<div className="sideNav">
<div
className="batman"
onClick={()=>handleHeroSelect('batman')}
/>
<div
className="superman"
onClick={()=>handleHeroSelect('superman')}
/>
<div
className="greenlantern"
onClick={()=>handleHeroSelect('greenlantern')}
/>
</div>
)
}
import React from 'react'
import ActionHeroStoreGoods from './ActionHeroStoreGoods'
export default function ActionHeroStore(props) {
const {hero}=props
return (
<div className={`actionHeroStore ${hero}div`}>
<h3>{hero}</h3>
<div className="actionHeroStore_goods">
<ActionHeroStoreGoods hero={hero}/>
</div>
</div>
)
}
import React, { Fragment,useContext } from 'react'
import {HeroContext} from './App'
export default function ActionHeroStoreGoods({hero}) {
const {giveHeroGoods,
handleUpdateGoods,
handleUpdateCart
}=useContext(HeroContext)
const goods=['tshirts','shoes','socks'];
const goodsElement=goods.map((element,index) => {
return <Fragment key={index}>
<div className="soloGood">
<span>{element}</span>
<button
onClick={
()=>decrement(hero,element)
}>-</button >
<span>{giveHeroGoods(hero,element)}</span>
<button onClick={
()=>{
increment(hero,element)
handleUpdateCart(hero)
}
}>+</button>
</div>
</Fragment>
})
function increment(hero,element){
let updateObj={};
updateObj[element]=giveHeroGoods(hero,element)+1
handleUpdateGoods(hero,updateObj)
}
function decrement(hero,element){
if(giveHeroGoods(hero,element)>0)
{
let updateObj={};
updateObj[element]=giveHeroGoods(hero,element)-1
handleUpdateGoods(hero,updateObj)
}
}
return (
<>
{goodsElement}
</>
)
}
The problem is not in setState. The problem in the code. handleUpdateCart() function is called before the *Goods states are changed. So It works with old data. If you will add in the your 'App.js' file the following fragment:
...
...
function giveHeroGoods(hero,element)
{
return (
hero=='batman'?batmanGoods[element]:
hero=='superman'?supermanGoods[element]:
hero=='greenlantern'?greenLanternGoods[element]:empty()
)
}
// FROM HERE
React.useEffect(() => {
handleUpdateCart('batman');
}, [
batmanGoods
]);
React.useEffect(() => {
handleUpdateCart('superman');
}, [
supermanGoods
]);
React.useEffect(() => {
handleUpdateCart('greenlantern');
}, [
greenLanternGoods
]);
// TILL HERE
function handleUpdateCart(name){
...
...
It is console logging the right array out all the time, but the point here is that it should be outputting that in the 'TodoList.tsx'. Not sure how to get that fixed in this case. Anyone who could help me with this. To see the bigger picture, please click on this link:
Link to codesandbox todo
I want the returned value from App.js currentFilter function pass it to TodoListItem.js, so it will update the map function constantly when user clicks on filter buttons.
// TodoFilter
import React from 'react';
interface TodoListFilter {
currentFilter: CurrentFilter;
}
export const TodoFilter: React.FC<TodoListFilter> = ({ currentFilter }) => {
return (
<ul>
Filter
<li onClick={() => currentFilter('All')}>All</li>
<li onClick={() => currentFilter('Complete')}>Completed</li>
<li onClick={() => currentFilter('Incomplete')}>Incompleted</li>
</ul>
)
}
// App.js
const currentFilter: CurrentFilter = filterTodo => {
let activeFilter = filterTodo;
switch (activeFilter) {
case 'All':
return todos;
case 'Complete':
return todos.filter(t => t.complete);
case 'Incomplete':
return todos.filter(t => !t.complete);
default:
console.log('Default');
}
}
return (
<React.Fragment>
<TodoList
todos={todos}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
editTodo={editTodo}
saveEditedTodo={saveEditedTodo}
getEditText={getEditText}
/>
<TodoFilter currentFilter={currentFilter}/>
<AddTodoForm addTodo={addTodo}/>
</React.Fragment>
)
// TodoListItem
import React from 'react';
import { TodoListItem } from "./TodoListItems";
interface TodoListProps {
todos: Array<Todo>;
toggleTodo: ToggleTodo;
deleteTodo: DeleteTodo;
editTodo: EditTodo;
getEditText: GetEditText;
saveEditedTodo: SaveEditedTodo;
currentFilter: CurrentFilter;
}
export const TodoList: React.FC<TodoListProps> = ({ todos, toggleTodo, deleteTodo, editTodo, getEditText, saveEditedTodo, currentFilter }) => {
return (
<ul>
{todos.map((todo, i) => {
return <TodoListItem key={i}
todo={todo}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
editTodo={editTodo}
saveEditedTodo={saveEditedTodo}
getEditText={getEditText}
/>
})}
</ul>
)
}
//Folder structure
src
-App.tsx
-AddTodoForm.tsx
-TodoFilter.tsx
-TodoList.tsx
The reason why the list not updating is that currentFilter passed as a prop to TodoList component is not used there at all.
Please consider two ways of solving it:
Pass a full list + filter object and apply filter inside TodoList
Apply filter object on the list at App component level and pass filtered list to TodoList component.
Personally I would go with the second approach but it's up to you :)
You need to create two arrays.One is original and second is filtered like this in your example.
const [todos, setTodos] = useState(initialTodos);
const [filtered, setFiltered] = useState(initialTodos);
Now you need to send filtered array in list component.Any updation or deletion you have to make on your todos array.And in currentFilter,you have to filter from original array that is todos and set it to filtered array in like this:
useEffect(() => {
setFiltered(todos);
}, [todos]);
Link of forked sandbox : link
Let me know if this helps you.