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>
);
};
Related
This question already has answers here:
State not updating when using React state hook within setInterval
(14 answers)
Closed 5 months ago.
I'm working on building a file upload portal in React that allows for multiple concurrent uploads, using an amalgamation of some preexisting code and new code that I'm writing myself. The code base is pretty complex, so I will explain what's happening at a high level, illustrate the problem, and then provide a toy example to simulate what's happening in CodeSandbox.
The top level component has a files state variable from useState, which is an object that contains sub objects with information regarding each file that the user is currently uploading, that is being mapped across in the JSX and returning UI elements for each. Think:
const uploadData = {
1: {
id: 1,
name: "File 1",
progress: 0
},
2: {
id: 2,
name: "File 2",
progress: 0
}
};
const [files, setFiles] = useState(uploadData);
const progressElements = Object.values(files).map((file) => (
<Progress key={file.id} value={file.progress} />
))
When an upload is initiated, existing code dictates that a callback is provided from the top level that receives an updated progress value for a given upload, and then sets that into state in files for the corresponding upload. This works perfectly fine when there is only one active upload, but as soon as a second file is added, the fact that the same files state object is being concurrently updated in multiple places at once bugs out the UI and causes the rendered JSX to be inaccurate. What is the correct way to handle concurrent updates to the same state at once?
Below is a super simplified (and hastily written, my apologies) sandbox as a toy example of what's going on. It's obviously not an exact replica of what's happening in the actual code, but it gets the general idea across. You can see that, with one upload going, the UI updates fine. But when additional uploads are added, any updates to the first overwrite the existence of the new upload in state and thus break the UI.
https://codesandbox.io/s/elastic-wozniak-yvbfo6?file=/src/App.js:133-297
const { useState, useEffect } = React;
const App = () => {
const uploadData = {
1: {
id: 1,
name: "File 1",
progress: 0
}
};
const [files, setFiles] = useState(uploadData);
const [uploading, setUploading] = useState(false);
const updateProgress = (uploadId, progress) => {
setFiles({
...files,
[uploadId]: {
...files[uploadId],
progress
}
});
};
const filesArray = Object.values(files);
const addNewFile = () => {
const lastUpload = files[filesArray.length];
const newId = lastUpload.id + 1;
setFiles({
...files,
[newId]: {
id: newId,
name: 'File ' + newId,
progress: 0
}
});
};
return (
<div className="App">
{filesArray.map((file) => (
<UploadStatus
key={file.id}
uploading={uploading}
setUploading={setUploading}
file={file}
updateProgress={updateProgress}
/>
))}
<button
onClick={
uploading ? () => setUploading(false) : () => setUploading(true)
}
>
{uploading ? "Cancel Upload" : "Start Upload"}
</button>
{uploading && <button onClick={addNewFile}>Add New File</button>}
</div>
);
}
const UploadStatus = ({
file,
updateProgress,
uploading,
setUploading
}) => {
useEffect(() => {
if (!uploading) return;
let calls = 0;
const interval = setInterval(() => {
calls++;
updateProgress(file.id, calls * 10);
}, 1000);
if (calls === 10) {
clearInterval(interval);
setUploading(false);
}
return () => clearInterval(interval);
}, [uploading]);
return (
<div key={file.id}>
<p>{file.name}</p>
<progress value={file.progress} max="100" />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>
<body>
<div id="root"></div>
</body>
Any help or thoughts would be greatly appreciated. Thanks!
Based on what is inside your codesandbox - the issue is just with closures and missing items in dependency array of useEffect. I do understand why you want to have only the things you described in depsArray of useEffect - but if you dont provide all the things there - you will get an issue with closure and you will be calling the old function or refering old variable, what is happening in your codesandbox. Consider wrapping everything that is passed to the hooks or child components with useMemo, useCallback and other memoizing functions react provide and include everything that is needed in depsArray. There is a workaround with useRef hook to hold a reference to the specific function you want to call + useEffect with the only purpose to update this useRef variable.
So rough fix 1:
import { useEffect, useRef } from "react";
export default function UploadStatus({
file,
updateProgress,
uploading,
setUploading
}) {
const updateProgressRef = useRef(updateProgress);
const setUploadingRef = useRef(setUploading);
useEffect(() => {
updateProgressRef.current = updateProgress;
}, [updateProgress]);
useEffect(() => {
setUploadingRef.current = setUploading;
}, [setUploading]);
useEffect(() => {
if (!uploading) return;
let calls = 0;
const interval = setInterval(() => {
calls++;
updateProgressRef.current(file.id, calls * 10);
if (calls === 10) {
clearInterval(interval);
setUploadingRef.current(false);
}
}, 1000);
return () => clearInterval(interval);
}, [file.id, uploading]);
return (
<div key={file.id}>
<p>{file.name}</p>
<progress value={file.progress} max="100" />
</div>
);
}
You will have to fix some logic here due to when any of the Uploadings is done - all the rest uploading will "stop" due to uploading and setUploading parameters.
What can simplify the fixing process is to modify addNewFile and updateProgress so they will not rely on closure captured files. setXXX functions from useState can recieve either the new value as a parameter, either a callback currentValue => newValue. So you can use callback one:
So rough fix 2:
const updateProgress = (uploadId, progress) => {
setFiles((files) => ({
...files,
[uploadId]: {
...files[uploadId],
progress
}
}));
};
const addNewFile = () => {
const lastUpload = files[filesArray.length];
const newId = lastUpload.id + 1;
setFiles((files) => ({
...files,
[newId]: {
id: newId,
name: `File ${newId}`,
progress: 0
}
}));
};
This fix will actually work better but still, wirings of states and components and depsArray should be fixed more precisiolly.
Hope that helps you to get the basic idea of what is going on and in which direction to dig with your issues.
I am currently get this duplicated key warning "Warning: Encountered two children with the same key". However, I am unsure where this duplication of key comes from. I am using the fileData id as my key which should be unique as it is firebase generated id. Therefore, I am not so sure what is happening behind here.
Here are my codes below and the warning I get.
MultimediaDetails.js
import React, { useEffect, useState } from "react";
import * as AiIcons from "react-icons/ai";
import * as FaIcons from "react-icons/fa";
import { database } from "../../../firebase";
import ViewImageFileModal from "../../modals/multimediaModals/view/ViewImageFileModal";
/**
* It's a component that displays audio, video, and image files
* #param props - The props object that is passed to the component.
* #returns The MultimediaDetails component is being returned.
*/
const MultimediaDetails = (props) => {
/* Destructuring the props object. */
const { pId } = props;
/* Setting the state of the component. */
const [imageData, setImageData] = useState([]);
const [imageMessage, setImageMessage] = useState(true);
const userType = JSON.parse(localStorage.getItem("admin") ?? false);
// Modal Variables
const [showViewImageModal, setShowViewImageModal] = useState(false);
const [fileData, setFileData] = useState(Object);
/**
* When the user clicks on the audio, video, or image file, the file data is set and the modal is
* toggled.
* #param obj
*/
const viewImageFile = (obj) => {
setFileData(obj);
toggleViewImageModal();
};
/* The function to toggle modal states */
const toggleAddImageModal = () => setShowAddImageModal((p) => !p);
const toggleViewImageModal = () => setShowViewImageModal((p) => !p);
useEffect(() => {
/* Query data from database and listening for changes. */
const imageQuery = database.portfolioRef.doc(pId).collection("images");
const unsubscribeImage = imageQuery.onSnapshot((snapshot) => {
if (snapshot.docs.length !== 0) {
setImageMessage(false);
setImageData(
snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id }))
);
} else {
setImageMessage(true);
}
});
return () => {
unsubscribeImage();
};
}, [pId]);
return (
<div className="multimedia-section">
<div id="image-section">
<div id="image-header">
<h6>
<u>Images</u>
</h6>
{userType ? (
<button className="addbtn" onClick={() => toggleAddImageModal()}>
<AiIcons.AiOutlinePlus /> Add Image File
</button>
) : (
<></>
)}
</div>
<div id="image-content" className="multimedia-flex">
{imageMessage ? (
<p>There is not existing images for this portfolio.</p>
) : (
<div>
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
</div>
)}
</div>
</div>
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
data={fileData}
key={fileData.id}
/>
</div>
);
};
export default MultimediaDetails;
The initialised values for the Modal.
/* Setting the initial state of the component. */
const valueState = {
name: '',
description: ''
}
const { currentUser } = useAuth();
const [formStateDisabled, setFormStateDisabled] = useState(true);
const [deleteState, setDeleteState] = useState(false);
const [message, setMessage] = useState('');
const [imageUrl, setImageUrl] = useState("");
const [loadForm, setLoadForm] = useState(false)
const [view, setView] = useState(false);
/* Destructuring the props object. */
const { show, toggleModal } = props;
const { handleChange, handleSubmit, values, errors, loading } =
useForm(validateUpdate, valueState, handleUpdate);
useEffect(() => {
if (Object.keys(props.data).length !== 0) {
values.name = props.data.imageName;
values.description = props.data.imageDesc;
setLoadForm(true);
}
}, [])
The warning I get (Shown Below), each time I click on the modal button to open the button, I noticed the warning actually repeats twice, and when I close it, it repeats another 2 times making it 4. I am not sure what is the cause of this, please help! Thank you!
Updates of trials
I only have 4 rows of data, all of which has its own unique id. Therefore I am unsure of where the duplicated key came from. However, if I remove the modal key "fileData.id" this warning would disappear. However, my component state will not reset and there will be a lot of props data issue that would surface. Where data for the previously clicked button will appear on the another button. Or the data might not appear at all.
FOR ADDITIONAL INFORMATION:
This is the output for the map buttons
I don't see any duplicates, and I am not sure where the issue is. Is there something I am doing wrong to cause this error. I checked my DB there isn't any data error as well.
Recommended solution
The problem is that your doc.id is repeating.
You are setting the imageData at the imageQuery.onSnapshot callback function, when you run the following code:
setImageData(snapshot.docs.map((doc) => ({ ...doc.data(), id: doc.id })));
What you need to make sure is that doc.id is unique in this context (because you're using this value at the key attribute in your buttons).
That's the correct way to fix it.
Alternative solution
Another way to handle it (as a last resort), is using the following code, where you use the index position of the element at the key attribute:
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
But this is not recommended according to the React documentation:
We don’t recommend using indexes for keys if the order of items may
change. This can negatively impact performance and may cause issues
with component state. Check out Robin Pokorny’s article for an
in-depth explanation on the negative impacts of using an index as a key.
If you choose not to assign an explicit key to list items then
React will default to using indexes as keys.
Here is an in-depth explanation about why keys are necessary if you’re
interested in learning more.
Instead of doc.id use map item index like bellow. See if it works.
{imageData.map((index, doc) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
This is the culprit:
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
The main issue here is that doc.id is duplicate, probably you have duplicate data in your imageData or ou have a faulty data in your database or something that generate a non-unique id.
To easily fix the issue, what you can do is use index of map.
{imageData.map((doc, index) => (
<button
key={index}
className="fileBtn"
onClick={() => viewImageFile(doc)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
index are always unique. but I suggest you should fix and see why you have duplicate data instead of just bypassing it with an index.
UPDATE
This is quite a hacky solution, but since I can't really pin point what's causing the issue without investigating first hand, let's make it so you don't have to pass a key on the modal.
So instead of storing the object data on the state, store the id instead:
Rename fileData to fileDataId.
const [fileDataId, setFileDataId] = useState(0);
then store the id when clicking the button.
{imageData.map((doc) => (
<button
key={doc.id}
className="fileBtn"
onClick={() => viewImageFile(doc.id)}
>
<FaIcons.FaImage /> {doc.imageName}
</button>
))}
on the Modal, you have to pass the imageData and the selected id, then remove the key:
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
pId={pId}
list={imageData}
selectedId={fileDataId}
/>
then inside ViewImageFileModal you can declare data as:
const data= props.list.find(image => image.id === props.selectedId);
I just wrap my model with a condition if imageId exist and it works already!
{imageId !== '' &&
<ViewImageFileModal
show={showViewImageModal}
toggleModal={toggleViewImageModal}
imageData={imageData}
key={imageId}
/>
}
I'm building a ReactJS Component that uses React Awesome Slider.
What I'm trying to create is a slider with a description div under it, which changes the text then I change the Slide.
Now, I found a way to make it work but I have a problem with the setState of an object, here is the code.
SLIDER:
const AutoplaySlider = withAutoplay(AwesomeSlider);
const StaticSlider = ({slider}) => {
var images = "";
var length=0;
const [current, setCurrent] = useState(0);
const [title, setTitle] = useState([]);
const [description, setDescription] = useState([]);
switch (slider) {
case 'portfolio_sviluppo_software':
images = portfolio_description.sviluppo_software;
length= portfolio_description.sviluppo_software.length;
break;
case 'portfolio_domotica':
images = portfolio_description.domotica;
length= portfolio_description.domotica.length;
break;
case 'portfolio_digital_signage':
images = portfolio_description.digital_signage;
length= portfolio_description.digital_signage.length;
break;
case 'portfolio_ricerca_e_sviluppo':
images = portfolio_description.ricerca_e_sviluppo;
length= portfolio_description.ricerca_e_sviluppo.length;
break;
}
useEffect(
() => {
setTitle(
images.map(
(slide) => (slide.title)
)
);
setDescription(
images.map(
(desc) => (desc.data)
)
);
}, [images]
);
return(
<div>
<AutoplaySlider
play={true}
cancelOnInteraction={true}
interval={0}
onTransitionStart={slide => setCurrent(slide.nextIndex)}
className="sliderHome"
>
{images.map((image, index) => {
let src = "/image/slider/portfolio/"+image.image;
//console.log(src);
return (
<div key={index} data-src={src}>
</div>
);
})}
</AutoplaySlider>
<GalleryCaption selected={current} title={title} description={description} area={slider}/>
</div>
)
};
export default StaticSlider;
DESCRIPTION GENERATOR
const GalleryCaption = ({ selected = 0, title = [], description= [], area = 0 }) => {
const formattedIndex = selected + 1;
var title = title[selected];
var data = description[selected];
return (
<div className="containerDivDescriptionPortflio">
<div className="DivDescriptionPortflio">
<p id ={"description_portfolio_"+area} className="paragDescriptionPortflio" >
<h4>{title}</h4>
<hr></hr>
{
data.map((val) => (
<div className="rowDescriptionPortfolio">
<div className="divIndexPortfolio" dangerouslySetInnerHTML={{ __html: val.index }} >
</div>
<div className="divTextPortfolio" dangerouslySetInnerHTML={{ __html: val.text }} >
</div>
</div>
))}
</p>
</div>
</div>
);
};
export default GalleryCaption;
OBJECT EXAMPLE
{
"title":"text",
"data":[
{
"index":"text",
"text": "text"
},
{
"index":"text",
"text": "text"
}
],
"image": "folder/image.jpg"
},
(This is an element of an array of this kind of object)
Now the main problem is that if inside the use effect I only call the setTitle function all works as it should, but if I use also the setDescription all just stop working. I didn't get a specific error, but I get a white screen.
ERROR THAT I GET
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `PortfolioArea`. See https://reactjs.org/link/warning-keys for more information.
at div
at PortfolioArea (http://localhost:3000/static/js/bundle.js:1618:5)
at http://localhost:3000/static/js/bundle.js:4509:78
at Routes (http://localhost:3000/static/js/bundle.js:177110:5)
at Router (http://localhost:3000/static/js/bundle.js:177043:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:176523:5)
at App
at AppProvider (http://localhost:3000/static/js/bundle.js:3289:5)
Warning: Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details.
* Move data fetching code or side effects to componentDidUpdate.
* If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://reactjs.org/link/derived-state
Please update the following components: AwesomeSlider
The above error occurred in the <GalleryCaption> component:
at GalleryCaption (http://localhost:3000/static/js/bundle.js:973:5)
at div
at StaticSlider (http://localhost:3000/static/js/bundle.js:3049:5)
at div
at PortfolioArea (http://localhost:3000/static/js/bundle.js:1618:5)
at http://localhost:3000/static/js/bundle.js:4510:78
at Routes (http://localhost:3000/static/js/bundle.js:177111:5)
at Router (http://localhost:3000/static/js/bundle.js:177044:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:176524:5)
at App
at AppProvider (http://localhost:3000/static/js/bundle.js:3290:5)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.`
I've tried to change the useEffect second parameters to null and also to use a unique state for every parameter, but the problem seems to be that every time I try to set a state with an object inside the useEffect, on the first render I always get a null value inside that state.
Any tips?
I think that in StaticSlider, since images and length are calculated based on slider prop, I suggest using useMemo() to calculate them per slider change, instead of reassigning the values to the var variables, which is not how it should be done in React and invites bugs to come.
const StaticSlider = ({slider}) => {
const images = useMemo(
() => {
// calculate and return images value
// with `switch`
switch (slider) {
// ...
default:
return [];
}
},
[slider]
);
const length = useMemo(
() => {
// calculate and return length value
// with `switch`
switch (slider) {
// ...
default:
return 0;
}
},
[slider]
);
Please note that your current switch block does not have a default case, you should consider returning a default case with initial values for images and length.
Also note that the initial assignment of images is images = "" which would make it a string, but inside setDescription() you are calling images.map() which is an array method, so it won't work at the initial render of the component when images is an empty string. I think this is what causes the bug.
images = [] or default: return [] for images inside switch statement should be better.
Lastly I think you can consider using a condition check inside useEffect() to only setState on title and description when images is already populated (has length).
useEffect(
() => {
if (images.length) {
setTitle(
images.map(
(slide) => (slide.title)
)
);
setDescription(
images.map(
(desc) => (desc.data)
)
);
}
},
[images]
);
I'm trying to send a delete request to delete an item from an API.
The API request is fine when clicking on the button. But Item get's deleted only after refreshing the browser!
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
This is what my code looks like.
import React, {useState} from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState("")
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted()
}
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
)
};
export default Hamster;
<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>
Imagine you have a parent component (say HamstersList) that returns/renders list of these Hamster components - it would be preferable to declare that deleteHamster method in it, so it could either: a) pass some prop like hidden into every Hamster or b) refetch list of all Hamsters from the API after one got "deleted" c) remove "deleted" hamster from an array that was stored locally in that parent List component.
But since you are trying to archive this inside of Hamster itself, few changes might help you:
change state line to const [hamsterDeleted, setHamsterDeleted] = useState(false)
call setHamsterDeleted(true) inside of deleteHamster method after awaited fetch.
a small tweak of "conditional rendering" inside of return, to actually render nothing when current Hamster has hamsterDeleted set to true:
return hamsterDeleted ? null : (<div>*all your hamster's content here*</div>)
What do you want to do in the case the hamster is deleted? If you don't want to return anything, you can just return null.
I'm not too sure if I should add any parameter to SetHamsterDeleted for it to work?
Yes, I'd make this a boolean instead. Here's an example:
import React, { useState } from "react";
const Hamster = (props) => {
const [hamsterDeleted, setHamsterDeleted] = useState(false);
async function deleteHamster(id) {
const response = await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsterDeleted(true);
}
if (hamsterDeleted) return null;
return (
<div>
<p className={props.hamster ? "" : "hide"}>
{hamsterDeleted}
</p>
<button onClick={() => deleteHamster(props.hamster.id)}>Delete</button>
<h2>{props.hamster.name}</h2>
<p>Ålder:{props.hamster.age}</p>
<p>Favorit mat:{props.hamster.favFood}</p>
<p>Matcher:{props.hamster.games}</p>
<img src={'./img/' + props.hamster.imgName} alt="hamster"/>
</div>
);
};
HOWEVER! Having each individual hamster keep track of its deleted state doesn't sound right (of course I don't know all your requirements but it seems odd). I'm guessing that you've got a parent component which is fetching all the hamsters - that would be a better place to keep track of what has been deleted, and what hasn't. That way, if the hamster is deleted, you could just not render that hamster. Something more like this:
const Hamsters = () => {
const [hamsers, setHamsters] = useState([]);
// Load the hamsters when the component loads
useEffect(() => {
const loadHamsters = async () => {
const { data } = await fetch(`/hamsters`, { method: "GET" });
setHamsters(data);
}
loadHamsters();
}, []);
// Shared handler to delete a hamster
const handleDelete = async (id) => {
await fetch(`/hamsters/${id}`, { method: "DELETE" });
setHamsters(prev => prev.filter(h => h.id !== id));
}
return (
<>
{hamsters.map(hamster => (
<Hamster
key={hamster.id}
hamster={hamster}
onDelete={handleDelete}
/>
))}
</>
);
}
Now you can just make the Hamster component a presentational component that only cares about rendering a hamster, eg:
const Hamster = ({ hamster, onDelete }) => {
const handleDelete = () => onDelete(hamster.id);
return (
<div>
<button onClick={handleDelete}>Delete</button>
<h2>{hamster.name}</h2>
<p>Ålder:{hamster.age}</p>
<p>Favorit mat:{hamster.favFood}</p>
<p>Matcher:{hamster.games}</p>
<img src={'./img/' + hamster.imgName} alt="hamster"/>
</div>
);
};
I have a page wherein there are Listings.
A user can check items from this list.
Whenever the user checks something it gets added to a globally declared Set(each item's unique ID is added into this set). The ID's in this set need to be accessed by a seperate Component(lets call it PROCESS_COMPONENT) which processes the particular Listings whose ID's are present in the set.
My Listings code roughly looks like:
import React from "react";
import { CheckBox, PROCESS_COMPONENT } from "./Process.jsx";
const ListItem = ({lItem}) => {
return (
<>
//name,image,info,etc.
<CheckBox lId={lItem.id}/>
</>
)
};
function Listings() {
// some declarations blah blah..
return (
<>
<PROCESS_COMPONENT /> // Its a sticky window that shows up on top of the Listings.
//..some divs and headings
dataArray.map(item => { return <ListItem lItem={item} /> }) // Generates the list also containing the checkboxes
</>
)
}
And the Checkbox and the PROCESS_COMPONENT functionality is present in a seperate file(Process.jsx).
It looks roughly like:
import React, { useEffect, useState } from "react";
let ProcessSet = new Set(); // The globally declared set.
const CheckBox = ({lID}) => {
const [isTicked, setTicked] = useState(false);
const onTick = () => setTicked(!isTicked);
useEffect( () => {
if(isTicked) {
ProcessSet.add(lID);
}
else {
ProcessSet.delete(lID);
}
console.log(ProcessSet); // Checking for changes in set.
}, [isTicked]);
return (
<div onClick={onTick}>
//some content
</div>
)
}
const PROCESS_COMPONENT = () => {
const [len, setLen] = useState(ProcessSet.size);
useEffect( () => {
setLen(ProcessSet.size);
}, [ProcessSet]); // This change is never being picked up.
return (
<div>
<h6> {len} items checked </h6>
</div>
)
}
export { CheckBox, PROCESS_COMPONENT };
The Set itself does get the correct ID values from the Checkbox. But the PROCESS_COMPONENT does not seem to be picking up the changes in the Set and len shows 0(initial size of the set).
I am pretty new to react. However any help is appreciated.
Edit:
Based on #jdkramhoft
's answer I made the set into a state variable in Listings function.
const ListItem = ({lItem,set,setPSet}) => {
//...
<CheckBox lID={lItem.id} pset={set} setPSet={setPSet} />
)
}
function Listings() {
const [processSet, setPSet] = useState(new Set());
//....
<PROCESS_COMPONENT set={processSet} />
dataArray.map(item => {
return <ListItem lItem={item} set={processSet} setPSet={setPSet} />
})
}
And corresponding changes in Process.jsx
const CheckBox = ({lID,pset,setPSet}) => {
//...
if (isTicked) {
setPSet(pset.add(lID));
}
else {
setPSet(pset.delete(lID));
}
//...
}
const PROCESS_COMPONENT = ({set}) => {
//...
setLen(set.size);
//...
}
Now whenever I click the check box I get an error:
TypeError: pset.add is not a function. (In 'pset.add(lID)', 'pset.add' is undefined)
Similar error occurs for the delete function as well.
First of all, the set should be a react state const [mySet, setMySet] = useState(new Set()); if you want react to properly re-render with detected changes. If you need the set to be available to multiple components you can pass it to them with props or use a context.
Secondly, React checks if dependencies like [ProcessSet] has been changed with something like ===. Even though the items in the set are different, no change is detected because the object is the same and there is no re-render.
Update:
The setState portion of [state, setState] = useState([]); is not intended to mutate the previous state - only to provide the next state. So to update your set you would do something like:
const [set, setSet] = useState(new Set())
const itemToAdd = ' ', itemToRemove = ' ';
setSet(prev => new Set([...prev, itemToAdd]));
setSet(prev => new Set([...prev].filter(item => item !== itemToRemove)));
As you might notice, this makes adding and removing from a set as slow as a list. So unless you need to make a lot of checks with set.has() I'd recommend using a list:
const [items, setItems] = useState([])
const itemToAdd = ' ', itemToRemove = ' ';
setItems(prev => [...prev, itemToAdd]);
setItems(prev => prev.filter(item => item !== itemToRemove));