So I'm using NextJS and tailwind to create a notification provider for my app, however when I display multiple notifications and the top one gets removed the one underneath it takes over it's fade value, how do I fix this?
import { createContext, useState, useContext } from "react";
const Context = createContext();
const Provider = ({ children }) => {
const [notifications, setNotifications] = useState([]);
const exposed = {
addNotification: (type, text, autoClose) => {
const id = Math.random()
setNotifications(notifications => [...notifications, { id, type, text, autoClose, fade: false }]);
if (autoClose) {
setTimeout(() => {
removeNotification(id);
}, 5000);
}
},
};
const removeNotification = (id) => {
setNotifications(notifications => notifications.map(n => n.id === id ? { ...n, fade: true } : n));
setTimeout(() => {
setNotifications(notifications => notifications.filter(n => n.id !== id));
}, 1000);
}
return (
<Context.Provider value={exposed}>
{children}
{notifications.length > 0 ? <div className="z-50 fixed top-5 right-5 text-right">
{notifications.map((notification, index) => {
switch (notification.type) {
case 'info':
return <Info text={notification.text} key={index} remove={() => removeNotification(notification.id)} fade={notification.fade} />
/* other cases */
}
})}
</div> : null}
</Context.Provider>
);
}
function Info({ remove, text, fade }) {
return (
<div className={`flex items-center w-fit mt-2 mr-0 ml-auto transition-opacity ease-in duration-1000 ${!fade?'opacity-100':'opacity-0'}`}>
{/* content */}
</div>
)
}
export const useProvider = () => useContext(Context);
export default Provider;
There might be other issues to be addressed, but for a list with dynamically changing items, consider to avoid using index as unique key to prevent potential conflicts when the list changes.
Perhaps try use notification.id as key in the posted example:
<Context.Provider value={exposed}>
{children}
{notifications.length > 0 ? (
<div className="z-50 fixed top-5 right-5 text-right">
{notifications.map((notification, index) => {
switch (notification.type) {
case "info":
return (
<Info
text={notification.text}
// 👇 Use id as unique keys
key={notification.id}
remove={() => removeNotification(notification.id)}
fade={notification.fade}
/>
);
/* other cases */
}
})}
</div>
) : null}
</Context.Provider>
Related
When i select item all items are re-rendered. How can I make it render only the one I change. Only state that is changing is 'selected'. Items are always same. So how can i make that only one item is gonna re-rendered base on 'selected' state. Im passing in Item component 'selected' state and i think there is problem that i dont get it.
import { memo, useState } from "react";
const items = Array(100)
.fill(null)
.map((item, index) => {
return { ...item, id: crypto.randomUUID(), name: `hello - ${index + 1}` };
});
const App = () => {
const [selected, setSelected] = useState<string[]>([]);
const selectHandler = (isIncluded: boolean, id: string) => {
if (isIncluded) {
setSelected(prev => prev.filter(idx => idx !== id));
} else {
setSelected(prev => [...prev, id]);
}
};
const MemoizedItem = memo(Item);
return (
<div className="flex flex-col gap-5 items-center justify-center py-10">
{items.map(item => (
<MemoizedItem
key={item.id}
data={item}
selected={selected}
selectHandler={selectHandler}
/>
))}
</div>
);
};
export default App;
type PropsItem = {
data: { id: string; name: string };
selected: string[];
selectHandler: (isSelected: boolean, id: string) => void;
};
const Item = ({ data, selected, selectHandler }: PropsItem) => {
return (
<div className="cursor-pointer min-w-[170px] text-center p-2">
{selected.includes(data.id) ? (
<s className="text-2xl" onClick={() => selectHandler(true, data.id)}>
{data.name}
</s>
) : (
<p className="text-2xl" onClick={() => selectHandler(false, data.id)}>
{data.name}
</p>
)}
</div>
);
};
I tried using memo and useCallback but in this case is not working. Im clueless. Im learning and still dont get it.
NewsDetails
import React, { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{
state
.filter((a) => a.id === id)
.map((card, index) => (
<>
<div className="card" key={index}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
</>
))
}
</div>
)
}
export default NewsDetail
NewsItem
import React from 'react'
import clock from "../components/assets/img/Clock.svg"
import user from "../components/assets/img/User.svg"
import { Link } from 'react-router-dom'
const NewsItem = (props) => {
const { imageUrl, title, author, content, date, id } = props
return (
<Link className="col-lg-4 p-2" to={`/detail/${id}`}>
<div className="newsItem">
<img src={imageUrl} alt='newsPhoto' />
<h2>{id}</h2>
<div className="itemBody">
<p className='title'>{title}</p>
<div className="line"></div>
<p className='content'>{content}</p>
<div className="itemfooter">
<span><img src={clock} alt='clock' />{date}</span>
<span><img src={user} alt='user' />{author}</span>
</div>
</div>
</div>
</Link>
)
}
export default NewsItem
Home
import React, { useEffect, useState } from "react";
import NewsItem from "./NewsItem";
import SpinnerLoad from "./SpinnerLoad";
import { v4 as uuidv4 } from 'uuid';
const Home = (props) => {
const Category = [
"all",
"business",
"sports",
"world",
"technology",
"entertainment",
"science"
];
const { state, setState} = props;
const [loading, setLoading] = useState(false)
const fetchValue = (category) => {
fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`)
.then(res => res.json())
.then(res => {
setState(res.data)
setLoading(true)
})
.catch((error) => console.log(error))
console.log(state);
setLoading(false);
};
// const fetchValue = async () => {
// try {
// const data = await axios
// .get(`https://inshorts-api.herokuapp.com/news?category=sports`)
// .then(res => {
// console.log(res);
// setState(res.data)
// })
// setLoading(true)
// console.log(loading);
// } catch (e) {
// console.log(e);
// }
// }
const CategoryButton = ({ category }) => (
<button onClick={() => fetchValue(category)} style={{ textTransform: 'capitalize' }}>{category}</button>
);
useEffect(() => {
fetchValue('all')
},[])
return (
<>
<div className="header-bg">
<h1 className="mb-3">News</h1>
<div className="btns ">
{Category.map((value, index) => {
return <CategoryButton category={value} key={index} />;
})}
</div>
</div>
<div className="news">
<div className="container">
<div className="row">
{
!loading
? <SpinnerLoad/>
:
state.map((data,index) => {
return (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()}
key={index}
/>
);
})
}
</div>
</div>
</div>
</>
);
};
export default Home;
I have created a project with api. With categories it is possible to change the incoming data, but there is one thing where I want to get more detailed information when I click on the newsItem card. That api doesn't have id value, so I used uuid. Information corresponding to the id value should come with useParams. But it doesn't work. How can I fix this problem?
The first issue is that you are generating a GUID when rendering the state array which won't necessarily correlate to any data you are trying to match/filter by in the NewsDetail component.
state.map((data,index) => (
<NewsItem
imageUrl={data.imageUrl}
author={data.author}
title={data.title}
content={data.content}
date={data.date}
id={uuidv4()} // <-- new id each render cycle
key={index}
/>
))
You want to inject the id property when the data is fetch so that it's a stable reference that lives as long as the data does. In other words, it should be an intrinsic property of the data.
Example:
const fetchValue = async (category) => {
setLoading(true);
try {
const res = await fetch(`https://inshorts-api.herokuapp.com/news?category=${category}`);
const { data } = await res.json();
setState(data.map(el => ({
...el,
id: uuidv4(), // <-- map and inject id here
})));
} catch(error) {
console.log(error);
} finally {
setLoading(false);
}
};
...
state.map((data) => (
<NewsItem
key={data.id} // <-- use as React key
data={data} // <-- pass entire data object as prop
/>
))
NewsItem
const NewsItem = ({ data }) => {
const { imageUrl, title, author, content, date, id } = data;
return (
...
);
};
NewsDetail
const NewsDetail = ({ state }) => {
const { id } = useParams();
return (
<div>
{state
.filter((card) => card.id === id)
.map((card) => (
<div className="card" key={card.id}>
<h2>{card.title}</h2>
<h2>{card.content}</h2>
<img src={card.imageUrl} alt="" />
</div>
))
}
</div>
);
};
I need to toggle multiple blocks with true/false in react state, but true and false works for all blocks at the same time.
import { useState } from "react";
...
const [toggleThisElement, setToggleThisElement] = useState(false);
...
{
data.map((d, id) => {
return (
<div className="single-history" key={id}>
<div className="h-head">
click this div for toggle h-info block
</div>
<div className="h-info">toggling block</div>
</div>
)
})
}
Currently, all your toggle items share the same state. To avoid that create a separate component for toggling logic called ToggleItem as below.
import { useState } from "react";
const ToggleItem = ({ discription, id }) => {
const [toggleThisElement, setToggleThisElement] = useState(false);
return (
<div className="single-history" key={id}>
<button
className="h-head"
onClick={() => setToggleThisElement((prev) => !prev)}
>
click this btn for toggle h-info block {id}
</button>
{toggleThisElement && <div className="h-info">{discription}</div>}
</div>
);
};
export default function App() {
const data = ["first", "second", "third"];
return (
<>
{data.map((d, id) => {
return <ToggleItem id={id} discription={d} />;
})}
</>
);
}
I'm new to react.js and I want to apply the toggle feature at 'place-box' by using 'isOpen' state and my intention is it only works when I click single place-box div so I added onClick event at 'place-box' div. but all of the elements are toggled at the same time.
I guess it's because they all have the same class name.
how can I fix this?
import React, { useState, useEffect } from "react";
import { useQuery } from "#apollo/client";
import { FETCH_CITIES_QUERY } from "../../server/Data/RentQueries";
import PlaceResult from "../Rent/PlaceResult";
const CityResult = (props) => {
const [placeId, setPlaceId] = useState();
const [isOpen, setIsOpen] = useState(false);
const { loading, error, data } = useQuery(FETCH_CITIES_QUERY, {
variables: { cityName: cityName },
});
const showPlaceInfo = (placeId, e) => {
e.preventDefault();
setPlaceId(placeId);
setIsOpen((isOpen) => !isOpen);
};
return (
<div>
{data &&
data.cities.map((city) => {
return (
<div className="city-box">
{city.places.map((place) => {
return (
// this is place-box div and I added onClick event here
<div
className="place-box"
key={place.id}
onClick={(e) => {
e.stopPropagation();
showPlaceInfo(place.id, e);
}}
>
<li className="place-name">{place.name}</li>
{isOpen && (
<PlaceResult className="place-indiv" placeId={placeId} />
)}
{!isOpen && (
<div className="place-info-box">
<li>{place.address}</li>
{conditionCheck(city.condition)}
<li>{place.phone}</li>
</div>
)}
</div>
);
})}
</div>
);
})}
</div>
);
};
export default CityResult;
Your variable isOpen is used for all cities. If you change isOpen to true all place-boxes are opened. You should store the id of the currently opened city inside a variable and compare against it to check if the current city in the for loop should be opened.
import React, { useState, useEffect } from "react";
import { useQuery } from "#apollo/client";
import { FETCH_CITIES_QUERY } from "../../server/Data/RentQueries";
import PlaceResult from "../Rent/PlaceResult";
const CityResult = (props) => {
const [placeId, setPlaceId] = useState();
const [openedPlaceId, setOpenedPlaceId] = useState(undefined);
const { loading, error, data } = useQuery(FETCH_CITIES_QUERY, {
variables: { cityName: cityName },
});
const showPlaceInfo = (placeId, e) => {
e.preventDefault();
setPlaceId(placeId);
setOpenedPlaceId(placeId);
};
return (
<div>
{data &&
data.cities.map((city) => {
return (
<div className="city-box">
{city.places.map((place) => {
return (
// this is place-box div and I added onClick event here
<div
className="place-box"
key={place.id}
onClick={(e) => {
e.stopPropagation();
showPlaceInfo(place.id, e);
}}
>
<li className="place-name">{place.name}</li>
{openedPlaceId === place.id && (
<PlaceResult className="place-indiv" placeId={placeId} />
)}
{!(openedPlaceId === place.id) && (
<div className="place-info-box">
<li>{place.address}</li>
{conditionCheck(city.condition)}
<li>{place.phone}</li>
</div>
)}
</div>
);
})}
</div>
);
})}
</div>
);
};
export default CityResult;
This way only the clicked place will be opened.
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