React Duplicate components updating wrong state: hooks - reactjs

I'm a newbie to react, only been using it for a few days, so forgive me if this is a stupid question.
I have a file input component and an image thumbnail component, I use two duplicate file input components to update two different states then display the image from the different states in two different thumbnail components. I have unique keys set on all of the components, but only the state for the first component in the Dom is updated. When I add an image using the second file input, it updates the state belonging to the first file input.
I've tried looking for solutions and all of them state to use unique keys, which I think I have done properly.
let [certification, setCertification] = useState(null)
let [photoId, setPhotoId] = useState(null)
let handleUpdateCertificate = (e) =>{
let file = e.target.files[0]
console.log(file)
let path = URL.createObjectURL(file)
let newCertificate = {
'file': file,
'path' : path
}
setCertification(newCertificate)
}
let handleUpdatePhotoId = (e) => {
let file = e.target.photoidinput.files[0]
let path = URL.createObjectURL(file)
let newPhotoID = {
'file': file,
'path' : path
}
setPhotoId(newPhotoID)
}
My return html is:
<div className='justify-content-center margin-20' key='certificate-wrapper'>
<ImgThumbnail key={'certificate'} name={'certificate'} image=
{certification?.path} wrapperClass={'justify-content-center margin-20'}/>
</div>
<div className='justify-content-center margin-20'>
<FileInput key={'certificateinput'} name={'certificateinput'} labelText={<p
className='text-paragraph edit-btn-text'>Add Certificate</p>}
onChange={handleUpdateCertificate}
classWrapper={'edit-profile-responsive-btn-wrapper'}/>
</div>
<div className='justify-content-center margin-20 ' key='photo-Id'>
<ImgThumbnail key={'photoid'} name={'photoId'} image={photoId?.path}
wrapperClass={'justify-content-center margin-20'}/>
</div>
<div className='justify-content-center margin-20' key='photo-id-input-wrapper'>
<FileInput key={'photoidinput'} name={'photoidinput'} labelText={<p
className='text-paragraph edit-btn-text'>Add Photo ID</p>}
onChange={handleUpdatePhotoId}
classWrapper={'edit-profile-responsive-btn-wrapper'}/>
</div>

Okay I'll give you some hints and then give you the working example:
You don't need to set key attribute if you are writing JSX elements like that, you need that only if you render a list of elements from an array, to prevent useless re-rendering when the array updates.
use const instead of let when a variable is static, there is a lint rule about it !
Try to use DRY, your update Handlers share a lot of logic, if you are going to add more inputs that would be all code repetition.
Now the code:
import React, { useState } from 'react';
import './style.css';
export default function App() {
const [certification, setCertification] = useState(null);
const [photoId, setPhotoId] = useState(null);
const updateData = (file, cb) => {
const path = URL.createObjectURL(file);
const data = {
file: file,
path: path,
};
cb(data);
};
const handleUpdateCertificate = (e) => {
updateData(e.target.files[0], setCertification);
};
const handleUpdatePhotoId = (e) => {
updateData(e.target.files[0], setPhotoId);
};
return (
<div>
{certification && (
<div className="justify-content-center margin-20">
<ImgThumbnail
name={'certificate'}
image={certification?.path}
wrapperClass={'justify-content-center margin-20'}
/>
</div>
)}
<div className="justify-content-center margin-20">
<FileInput
id="certificate"
name={'certificateinput'}
labelText={
<p className="text-paragraph edit-btn-text">Add Certificate</p>
}
onChange={handleUpdateCertificate}
classWrapper={'edit-profile-responsive-btn-wrapper'}
/>
</div>
{photoId && (
<div className="justify-content-center margin-20 " key="photo-Id">
<ImgThumbnail
name={'photoId'}
image={photoId?.path}
wrapperClass={'justify-content-center margin-20'}
/>
</div>
)}
<div
className="justify-content-center margin-20"
key="photo-id-input-wrapper"
>
<FileInput
id="photo"
name={'photoidinput'}
labelText={
<p className="text-paragraph edit-btn-text">Add Photo ID</p>
}
onChange={handleUpdatePhotoId}
classWrapper={'edit-profile-responsive-btn-wrapper'}
/>
</div>
</div>
);
}
const FileInput = ({ id, labelText, ...props }) => (
<label htmlFor={id}>
{labelText}
<input id={id} style={{ display: 'none' }} type="file" {...props} />
</label>
);
const ImgThumbnail = ({ name, image }) => (
<div>
<img style={{ width: '100px', height: '100px' }} src={image} alt={name} />
</div>
);
This example works right, you were probably doing something wrong inside FileInput Component, remember that a label has to have an htmlFor attribute with the id of the input element you want to trigger.
Now, this code can be optimized and made more React style, since you might have more file inputs in the future, let's see how it can be optimized by creating reusable Components and compose them properly:
import React, { useState } from 'react';
import './style.css';
/* INPUTS IMAGE TYPES */
const inputs = [
{ type: 'photo', name: 'photo', label: 'Photo' },
{ type: 'certificate', name: 'certificate', label: 'Certificate' },
{ type: 'anotherType', name: 'anotherName', label: 'Another Input' },
];
export default function App() {
return (
<div>
{inputs.map((data) => (
<ImagePreviewer key={data.type} data={data} />
))}
</div>
);
}
const FileInput = ({ id, labelText, ...props }) => (
<label htmlFor={id}>
{labelText}
<input id={id} style={{ display: 'none' }} type="file" {...props} />
</label>
);
const ImgThumbnail = ({ name, image }) => (
<div>
<img src={image} alt={name} />
</div>
);
const ImagePreviewer = ({ data: { type, name, label } }) => {
const [image, setImage] = useState(null);
const updateData = (file, cb) => {
const path = URL.createObjectURL(file);
const data = {
file: file,
path: path,
};
cb(data);
};
const handleUpdate = (e) => {
updateData(e.target.files[0], setImage);
};
return (
<div>
{image && (
<div>
<ImgThumbnail name={'name'} image={image?.path} />
</div>
)}
<div>
<FileInput
id={name}
name={name}
labelText={<p>Add {label}</p>}
onChange={handleUpdate}
/>
</div>
</div>
);
};
A working demo HERE.

Related

Why is my JSX.Element not updating when the state is updated unless I wrap it in another JSX.Element?

I am working on a typescript react project, both of which are new to me. I have the following model viewer and the type below it that consumes it. I deleted some of the bottom type so it is easier to read, but basically, getData gets called in useEffect. When the data is obtained, getData calls selectModule which, among other things, gets images from the data and sets shownImages. This all works as expected and shownImages is being set with the data expected. But when viewer 3d gets created, shownImages is not yet set. And it does not get recreated when shown images is set. However, if I replace <Viewer3D items={shownImages} /> with <Review3dComponent></Review3dComponent>, it does get recreated when shownImages is set. And I would like to know why Review3dComponent gets updated but Viewer3D does not.
interface Viewer3DProps {
items: Image[] | Image;
}
const Viewer3D = ({ items }: Viewer3DProps) => {
return (
<div className="threed-viewer">
<OBJModel
items={items}
enableTransform={true}
position={{ x: 0, y: 0, z: 0 }}
texPath=""
style={{ height: `100% !important`, width: `100% !important` }}
/>
</div>
);
};
export default Viewer3D;
export const File3dReview: NextPage = (): ReactElement => {
{
const [shownImages, setShownImages] = useState<Image[]>([]);
const selectModule = (module: IModule) => {
setCurrentModule(module);
setCurrentGroups(module.group);
const images: Image[] = module.group.flatMap(
(group) => group.images,
) as unknown as Image[];
setShownImages(images);
console.log(`set images`, images);
};
const getData = useCallback(async () => {
//gets data and calls select module
}, []);
useEffect(() => {
getData();
}, [getData]);
const Review3dComponent = () => {
return (
<div>
<Viewer3D items={shownImages} />
</div>
);
};
return (
<div>
<div>
<header className="app-header">
<div className="app-navigation">
<Space size="middle">
<div className="ic-24 color-secondary">
<FiArrowLeft
onClick={() => {
router.back();
}}
/>
</div>
<div>{name}</div>
</Space>
<nav>
<Button shape="circle" onClick={toggleDrawer}>
<MenuOutlined />
</Button>
</nav>
</div>
</header>
</div>
<Viewer3D items={shownImages} />
{/* <Review3dComponent></Review3dComponent> */}
<DrawerComponent></DrawerComponent>
</div>
);
}
};
export default File3dReview;

TypeError: Cannot read property '0' of undefined when building my react app

while building my react app for deployment, I am getting this error
TypeError: Cannot read property '0' of undefined
when I am rending on port3000 I did not see this error but only get it while building the app.
Can anyone assist to resolve this?
import { useState } from "react";
import styles from "./Tabs.module.css"
const Tabs = ({ children}) => {
const [activeTab, setActiveTab] = useState (children [0].props.label);
const handleClick =( e, newActiveTab ) => {
e.preventDefault();
setActiveTab(newActiveTab);
}
return (
<div>
<ul className= {styles.tabs}>
{children.map ((tab) => {
const label = tab.props.label;
return (
<li
className= {label == activeTab ? styles.current : ""}
key= {label}
>
<a href="#" onClick={(e) => handleClick (e, label)}>{label}
</a>
</li>
)
})}
</ul>
{children.map ((tabcontent1) => {
if (tabcontent1.props.label == activeTab)
return (
<div key= {tabcontent1.props.label} className= {styles.content}>{tabcontent1.props.children}
</div>
);
})}
</div>
);
}
export default Tabs ;
In next js, when you don't put export const getServerSideProps = () => {} in your page then that page is automatically subjected to static side rendering. On development mode, you may see a lightening symbol on bottom-right. Anyway you can read the docs on data-fetching on nextjs. However, your issue on this situation can be easily fixed by setting the children through useEffect.
// handle null on your active tab render function
const [activeTab, setActiveTab] = useState(null);
useEffect(() => {
if(children.length)
children[0].props.label
}, [children])
Another Code Sample:
*A simple change in code structure and the way you are trying to do. It's on react but kind of same in next as well *
import React from "react";
const Tabs = ({ tabsData }) => {
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
const switchTabs = (index) => setActiveTabIndex(index);
return (
<div style={{ display: "flex", gap: 20, cursor: "pointer" }}>
{/* Here active tab is given a green color and non actives grey */}
{tabsData.map((x, i) => (
<div
key={i}
style={{ color: activeTabIndex === i ? "green" : "#bbb" }}
onClick={() => switchTabs(i)}
>
{x.label}
</div>
))}
{/* Show Active Tab Content */}
{tabsData[activeTabIndex].content}
</div>
);
};
export default function App() {
// You can place it inside tabs also in this case
// but lets say you have some states on this component
const tabsData = React.useMemo(() => {
return [
// content can be any component or React Element
{ label: "Profile", content: <p>Verify all Input</p> },
{ label: "Settings", content: <p>Settings Input</p> },
{ label: "Info", content: <p>INput info</p> }
];
}, []);
return (
<div className="App">
<Tabs tabsData={tabsData} />
</div>
);
}
and here is also a example sandbox https://codesandbox.io/s/serverless-night-ufqr5?file=/src/App.js:0-1219

How can i set the value of an input and dropdown on an useState?

i'm quite new using React, or programming actually haha.
I am practicing making an online store and I already have almost everything working correctly. My problem is that I want the customer to be able to select the size and quantity and set it on a useState to pass that information to the cart.
This is my code:
import { Image, Grid, Icon, Button, Form } from "semantic-ui-react";
import { size } from "lodash";
import classNames from "classnames";
import useAuth from "../../../hooks/useAuth";
import useCart from "../../../hooks/useCart";
import {
isFavoriteApi,
addFavoriteApi,
deleteFavoriteApi,
} from "../../../Api/favorite";
import CarouselScreenshots from "../CarouselScreenshots";
import TabsItem from "../TabsItem"
export default function HeaderItem(props) {
const { item } = props;
const { poster, title, screenshots } = item;
return (
<div>
<Grid className="header-item">
<Grid.Column mobile={16} tablet={6} computer={8}>
<Image src={poster.url} alt={title} fluid />
<CarouselScreenshots title={title} screenshots={screenshots} />
</Grid.Column>
<Grid.Column mobile={16} tablet={10} computer={8}>
<Info item={item} />
</Grid.Column>
</Grid>
</div>
);
}
function Info(props) {
const { item } = props;
const { title, summary, price, discount, url } = item;
const [isFavorites, setIsFavorites] = useState(false);
const [reloadFavorite, setReloadFavorite] = useState(false);
const { auth, logout } = useAuth();
const { addProductCart } = useCart();
const [sizeItem, setSizeItem] = useState(null);
const [quantity, setQuantity] = useState(null);
useEffect(() => {
(async () => {
const response = await isFavoriteApi(auth.idUser, item.id, logout);
if (size(response) > 0) setIsFavorites(true);
else setIsFavorites(false);
})();
setReloadFavorite(false);
}, [item, reloadFavorite]);
const addFavorite = async () => {
if (auth) {
await addFavoriteApi(auth.idUser, item.id, logout);
setReloadFavorite(true);
}
};
const deleteFavorite = async () => {
if (auth) {
await deleteFavoriteApi(auth.idUser, item.id, logout);
setReloadFavorite(true);
}
};
const sizes = [
{
key: 'Small',
text: 'Small',
value: 'Small',
name: 'size'
},
{
key: 'Medium',
text: 'Medium',
value: 'Medium',
name: 'size'
},
{
key: 'Large',
text: 'Large',
value: 'Large',
name: 'size'
},
]
return (
<>
<div className="header-item__title">
{title}
</div>
<div className="header-item__buy">
<div className="header-item__buy-price">
{/* <p>Public price: ${price} </p> */}
<div className="header-item__buy-price-actions">
{discount && <div className="header-item__buy-price-actions"> <p>-{discount}% </p>
<p>${(price - Math.floor(price * discount) / 100).toFixed(2)}</p></div>}
{!discount && <p>${price}</p>}
</div>
<p className="subtitle">Size</p>
<Form>
<Form.Dropdown
placeholder='Select size'
fluid
selection
options={sizes}
/>
<p>Quantity</p>
<Form.Input placeholder='1' />
</Form>
</div>
<div className="header-item__buy-btn-container">
<Button
className="header-item__buy-btn-container__buy-btn"
type="submit"
onClick={() => addProductCart(url)}
>
Buy Now
</Button>
<div className="heart-container" >
<Icon
name={isFavorites ? "heart" : "heart outline"}
className={classNames({ like: isFavorites })}
link
onClick={isFavorites ? deleteFavorite : addFavorite}
/>
</div>
</div>
</div>
<div
className="header-item__summary"
dangerouslySetInnerHTML={{ __html: summary }}
/>
<div className="tabs" >
<TabsItem />
</div>
</>
);
}
This is what you looking for.
With a functional component you will need to import useState hook, and set 2 states like this:
const [size, setSize] = useState("small");
const [quantity, setQuantity] = useState(0);
Then follow the documentation in the link above. You should ultimatly have something like
<select value={size} onChange={(e) => changeSize(e)}>. And in your changeSize function, you should use setSize function to set the state.

Why the wrong element is being updated only when uploading files?

I have built a component CreatePost which is used for creating or editing posts,
the problem is if I render this component twice even if I upload a file from the second component they are changed in the first one, why? Here is the code:
import FileUpload from "#components/form/FileUpload";
import { Attachment, Camera, Video, Writing } from "public/static/icons";
import styles from "#styles/components/Post/CreatePost.module.scss";
import { useSelector } from "react-redux";
import { useInput, useToggle } from "hooks";
import { useRef, useState } from "react";
import StyledButton from "#components/buttons/StyledButton";
import Modal from "#components/Modal";
import { post as postType } from "types/Post";
import Removeable from "#components/Removeable";
interface createPostProps {
submitHandler: (...args) => void;
post?: postType;
isEdit?: boolean;
}
const CreatePost: React.FC<createPostProps> = ({ submitHandler, post = null, isEdit = false }) => {
console.log(post);
const maxFiles = 10;
const [showModal, setShowModal, ref] = useToggle();
const [description, setDescription] = useInput(post?.description || "");
const user = useSelector((state) => state.user);
const [files, setFiles] = useState<any[]>(post?.files || []);
const handleFileUpload = (e) => {
const fileList = Array.from(e.target.files);
if (fileList.length > maxFiles || files.length + fileList.length > maxFiles) {
setShowModal(true);
} else {
const clonedFiles = [...files, ...fileList];
setFiles(clonedFiles);
}
e.target.value = "";
};
const removeHandler = (id) => {
const filtered = files.filter((file) => file.name !== id);
setFiles(filtered);
};
return (
<div className={styles.createPost}>
<div className={styles.top}>
<span>
<img src="/static/images/person1.jpg" />
</span>
<textarea
onChange={setDescription}
className="primaryScrollbar"
aria-multiline={true}
value={description}
placeholder={`What's on your mind ${user?.name?.split(" ")[0]}`}
></textarea>
{description || files.length ? (
<StyledButton
background="bgPrimary"
size="md"
className={styles.submitButton}
onClick={() => {
if (!isEdit)
submitHandler({
files: files,
author: { name: user.name, username: user.username },
postedTime: 52345,
id: Math.random() * Math.random() * 123456789101112,
comments: [],
likes: [],
description,
});
else {
submitHandler({
...post,
description,
files,
});
}
setDescription("");
setFiles([]);
}}
>
{isEdit ? "Edit" : "Post"}
</StyledButton>
) : null}
</div>
<div className={styles.middle}>
<div className={styles.row}>
{files.map((file) => {
return (
<Removeable
key={file.name + Math.random() * 100000}
removeHandler={() => {
removeHandler(file.name);
}}
>
{file.type.includes("image") ? (
<img src={URL.createObjectURL(file)} width={150} height={150} />
) : (
<video>
<source src={URL.createObjectURL(file)} type={file.type} />
</video>
)}
</Removeable>
);
})}
</div>
</div>
<div className={styles.bottom}>
<FileUpload
id="uploadPhoto"
label="upload photo"
icon={
<span>
<Camera /> Photo
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
accept="image/*"
/>
<FileUpload
id="uploadVideo"
label="upload video"
icon={
<span>
<Video /> Video
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
accept="video/*"
/>
<FileUpload
id="writeArticle"
label="write article"
icon={
<span>
<Writing /> Article
</span>
}
className={styles.fileUpload}
multiple
onChange={handleFileUpload}
/>
</div>
{showModal && (
<Modal size="sm" backdrop="transparent" ref={ref} closeModal={setShowModal.bind(null, false)} yPosition="top">
<p>Please choose a maximum of {maxFiles} files</p>
<StyledButton size="md" background="bgPrimary" onClick={setShowModal.bind(null, false)}>
Ok
</StyledButton>
</Modal>
)}
</div>
);
};
export default CreatePost;
Now on my main file I have:
const Main = () => {
const [posts, setPosts] = useState<postType[]>([]);
const addPost = (post: postType) => {
setPosts([post, ...posts]);
};
const editPost = (post: postType) => {
const updated = posts.map((p) => {
if (post.id === post.id) {
p = post;
}
return p;
});
setPosts(updated);
};
const deletePost = (id) => {
const filtered = posts.filter((post) => post.id !== id);
setPosts(filtered);
};
return (
<>
<CreatePost submitHandler={addPost} key="0" />
<CreatePost submitHandler={addPost} key="1"/>
{posts.map((post) => {
return <PostItem {...post} editHandler={editPost} key={post.id} deleteHandler={deletePost.bind(null, post.id)} />;
})}
</>
);
};
export default Main;
I tried to add/remove the key but doesn't change anything, also tried to recreate this problem in a simpler way in sandbox but I can't it works fine there. And the problem is only when I upload files not when I write text inside the <textarea/>
Note: The second in reality is shown dynamically inside a modal when clicked edit in a post, but I just showed it here for simplicity because the same problem occurs in both cases.
Okay after some hours of debugging I finally found the problem.
Because my <FileUpload/> uses id to target the input inside the <CreatePost/> the <FileUpload/> always had same it, so when I used <CreatePost/> more than 1 time it would target the first element that found with that id that's why the first component was being updated

How to push to array inside dynamic object using React Hooks?

In this small code that I've written I have created a dynamic object upon setting state and adding files to it. However, the new file overwrites the previous file. With no luck, I have tried using the spread operator inside the array brackets next to mappedFiles. It seems upon setting the state dynamically, I am not able to concatenate or push to the array inside the files object.
Here is the code...
import React, { useCallback, useState, useContext } from "react";
import { ImageUploadContext } from "../../Store";
import styles from "./commentssection.module.css";
import { useDropzone } from "react-dropzone";
function ImageUploader({ title }) {
const [files, setFiles] = useContext(ImageUploadContext);
const maxSize = 5048576;
//ISSUE IS HERE. CREATING THE SETSTATE INSIDE THIS CALLBACK
const onDrop = useCallback(
(acceptedFiles) => {
const mappedFiles = acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
// This setstate function adds to dynamic array but doesn't return previous one. The array is being overwritten
setFiles((state) => ({ ...state, [title]: [mappedFiles] }));
},
[files]
);
const {
isDragActive,
getRootProps,
getInputProps,
isDragReject,
acceptedFiles,
rejectedFiles,
} = useDropzone({
onDrop,
accept: "image/*",
minSize: 0,
maxSize: 10000000,
});
console.log(files);
const isFileTooLarge = rejectedFiles
? rejectedFiles.length > 0 && rejectedFiles[0].size > maxSize
: null;
return (
<div>
<p>Please include comments in notes</p>
<hr className={styles["HR"]} />
<form className={styles["UploadForm"]}>
<div className={styles["UploadWrapper"]}>
<h5>Upload photos of issues found in the {title}</h5>
<section className={styles["Container"]}>
<div className={styles["ImageInput"]} {...getRootProps()}>
<input {...getInputProps()} />
{!isDragActive && "Click here or drop a file to upload!"}
{isDragActive && !isDragReject && "Drop it like it's hot!"}
{isDragReject && "File type not accepted, sorry!"}
{isFileTooLarge && (
<div className="text-danger mt-2">File is too large.</div>
)}
</div>
</section>
<div>
{files[title]
? files[title].map((object, index) =>
object.map((subObject, subIndex) => {
return (
<img
style={{ height: "80px" }}
className={styles["RenderedImage"]}
key={index}
src={subObject.preview}
/>
);
})
)
: null}
</div>
<p>
Please take a picture of any issues that you find and upload them
here. NOTE: it is only necessary to upload pictures of problems that
you find.
</p>
</div>
<div className={styles["CommentWrapper"]}>
<h5>Notes of the {title}</h5>
<textarea className={styles["Textarea"]} />
</div>
</form>
</div>
);
}
export default ImageUploader;
Edit:
I was able to figure it out thanks to #lawrence-witt1 . Here is the code for the arrays to be parent component specific.
const onDrop = useCallback(
(acceptedFiles) => {
const mappedFiles = acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);
return files[title]
? setFiles((state) => ({
...state,
[title]: [...state[title], mappedFiles],
}))
: setFiles((state) => ({
...state,
[title]: [mappedFiles],
}));
},
[files, title]
);
I think I spotted the issue - you need to include title in the dependency array of onDrop:
const onDrop = useCallback(
(acceptedFiles) => {
...
},
[files, title]
);
Otherwise you run the risk of having a stale value for title which would overwrite your state object's property.
Edit:
I think this is what you were looking for:
setFiles((state) => ({ ...state, [title]: [...state[title], ...mappedFiles]}));
If your state object with array looks like below
var std={students:{names:["AAA","BBB","CCC"]}}
State
const[students,setstudents]=useState(std)
setState
setstudents({ ...students, ...students.names.push("DDD") })

Resources