change in one item re runs whole map component loop in ReacJS - javascript

I am dispatching an add comment action on a specific post re runs component loop again instead of updating a specific one. Suppose, If I have 100 posts adding comments to one post runs the component loop again and iterates again 100 times. Is there is any way to re-render only a specific item instead of running the whole component loop again?
Here's my code of the looped component
const Post = ({totalComments, like, _id, image, caption}) => {
const {enqueueSnackbar} = useSnackbar();
const dispatch = useDispatch();
const { user } = useSelector((state) => state.loadUser);
const { loading: loadingComments, comments } = useSelector((state) => state.listComments);
const { success: addCommentSucess, error: addCommentError } = useSelector((state) => state.addComment);
const [openComment, setOpenComment] = useState('');
const [commentsLength, setCommentsLength] = useState(totalComments);
//listing of comments
useEffect(() => {
if (addCommentError) {
enqueueSnackbar(addCommentError, {variant: 'error'});
dispatch(clearErrors());
}
}, [addCommentError, addCommentSucess, dispatch, enqueueSnackbar]);
const openCommentHandler = () => {
dispatch(listComments(_id));
setOpenComment(!openComment);
}
const addCommentHandler = (data) => {
console.log('addcomehandle')
dispatch(addComment(data));
setCommentsLength(commentsLength+1);
}
return (
<Card className='post-container'>
<div className='button-wrapper'>
<IconButton onClick={openCommentHandler} color={openComment ? 'primary' : 'default'}>
<SvgIcon component={CommentOutlinedIcon} />
</IconButton>
<p>{commentsLength}</p>
</div>
{!loadingComments && openComment && (
<Comments comments={comments} />
)}
<Divider />
<AddComment onAddComment={addCommentHandler} postId={_id} />
</Card>
);
};
export default Post;

Related

Why does my toast notification not re-render in React?

I am trying to create my own "vanilla-React" toast notification and I did manage to make it work however I cannot wrap my head around why one of the solutions that I tried is still not working.
So here we go, onFormSubmit() I want to run the code to get the notification. I excluded a bunch of the code to enhance readability:
const [notifications, setNotifications] = useState<string[]>([]);
const onFormSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
const newNotifications = notifications;
newNotifications.push("success");
console.log(newNotifications);
setNotifications(newNotifications);
};
return (
<>
{notifications.map((state, index) => {
console.log(index);
return (
<ToastNotification state={state} instance={index} key={index} />
);
})}
</>
</section>
);
Inside the Toast I have the following:
const ToastNotification = ({
state,
instance,
}:
{
state: string;
instance: number;
}) => {
const [showComponent, setShowComponent] = useState<boolean>(true);
const [notificationState, setNotificationState] = useState(
notificationStates.empty
);
console.log("here");
const doNotShow = () => {
setShowComponent(false);
};
useEffect(() => {
const keys = Object.keys(notificationStates);
const index = keys.findIndex((key) => state === key);
if (index !== -1) {
const prop = keys[index] as "danger" | "success";
setNotificationState(notificationStates[prop]);
}
console.log(state);
}, [state, instance]);
return (
<div className={`notification ${!showComponent && "display-none"}`}>
<div
className={`notification-content ${notificationState.notificationClass}`}
>
<p className="notification-content_text"> {notificationState.text} </p>
<div className="notification-content_close">
<CloseIcon color={notificationState.closeColor} onClick={doNotShow} />
</div>
</div>
</div>
);
};
Now for the specific question - I cannot understand why onFormSubmit() I just get a log with the array of strings and nothing happens - it does not even run once - the props get updated with every instance and that should trigger a render, the notifications are held into a state and even more so, should update.
What is wrong with my code?

Component returning nested React Elements not displaying

I have a default component Collection which uses a sub-component called RenderCollectionPieces to display UI elements. I can't figure out why I am able to see the data for image.name in the console but not able to see the UI elements display.
Additional information:
There are no errors in the console
If I replace <p>{image.name}</p> with <p>TESTING</p>, still nothing shows.
columnOrg is a list of lists
each list in columnOrg is a list of maps with some attributes
Index.js:
const RenderCollectionPieces = () => {
const {collectionId} = useParams();
let listOfImageObjects = collectionMap[collectionId];
let imagesPerColumn = Math.ceil(listOfImageObjects.length / 4);
let columnOrg = [];
while (columnOrg.length < 4){
if(imagesPerColumn > listOfImageObjects.length){
imagesPerColumn = listOfImageObjects.length;
}
columnOrg.push(listOfImageObjects.splice(0,imagesPerColumn))
}
let collectionList = columnOrg.map((col) => {
return(
<Grid item sm={3}>
{
col.map((image) => {
console.log(image.name)
return(
<p>{image.name}</p>
)
})
}
</Grid>
)
});
return collectionList;
};
const Collection = ({ match }) => {
const {collectionId} = useParams();
return(
<Box sx={{ background:'white'}}>
<Grid container>
<RenderCollectionPieces />
</Grid>
</Box>
)
};
export default Collection;
I think you are misunderstanding state management in React. Every variable you want to remember inbetween component re-renders should be included in state using useState hook. If you want to perform something initially like your while loop, use it inside useEffect hook.
const MyComponent = () => {
const [myCounter, setMyCounter] = useState(0);
useEffect(() => {
console.log("This will be performed at the start");
}, []);
return (
<Fragment>
<button onClick={() => setMyCounter(myCounter++)} />
You clicked {myCounter} times
</Fragment>
)
}
If you are unfamiliar with useState and useEffect hooks I recommend learning about them first to understand how React manages state and re-renders: https://reactjs.org/docs/hooks-intro.html
Got it to work by using useEffect/useState as recommended by Samuel Oleksak
const RenderCollectionPieces = (props) => {
const [columnOrg, setColumnOrg] = useState([]);
useEffect(() => {
let columnSetup = []
let listOfImageObjects = collectionMap[props.collectionId.collectionId];
let imagesPerColumn = Math.ceil(listOfImageObjects.length / 4);
while (columnSetup.length < 4){
if(imagesPerColumn > listOfImageObjects.length){
imagesPerColumn = listOfImageObjects.length;
}
columnSetup.push(listOfImageObjects.splice(0,imagesPerColumn))
}
setColumnOrg(columnSetup);
},[]);
return (
columnOrg.map((column) => {
return (
<Grid item sm={3}>
{
column.map((image) => {
return (<img src={image.src} alt={image.name}/>)
})
}
</Grid>
)
})
)
};

state going empty when using useEffect

I have 5 cities and their weather displayed on a card when you click one I want more details to appear on another card. I also want the detailed card to have initial data of the first city. Problem is when I try to set the clicked data to state in useEffect the state goes empty, even the initial data. If I add the dependency array [props.activeWeather] in useEffect I can click and it will show the data but the initial data won't show as the state shows empty. I'm using props to send the card the clicked city object as well as the first array of data.
const ActiveWeather = (props) => {
const [weather, setWeather] = useState(props.data);
//adding this cause the weather state to go empty?
useEffect(() => {
if (props.activeWeather) {
setWeather(props.activeWeather);
}
}, []);
console.log(weather);
return (
<Container>
<Card>
<Card.Header> {weather?.city}</Card.Header>
{weather?.temp}
</Card>
</Container>
);
};
export default ActiveWeather;
Parent Component
const fetchCity = async (city) => {
const res = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${key}`);
return {
description: res.data.weather[0].description,
icon: res.data.weather[0].icon,
temp: res.data.main.temp,
city: res.data.name,
country: res.data.sys.country,
id: res.data.id,
};
};
function App() {
const [data, setData] = useState([]);
const [activeWeather, setActiveWeather] = useState([]);
useEffect(() => {
const fetchCities = async () => {
const citiesData = await Promise.all(["Ottawa", "Toronto", "Vancouver", "California", "London"].map(fetchCity)).catch((err) => {
console.log("error:", err);
});
setData((prevState) => prevState.concat(citiesData));
};
fetchCities();
}, []);
const handleClick = (event) => {
const weather = JSON.parse(event.target.dataset.value);
setActiveWeather(weather);
};
return (
<div className="App">
<Header />
<Container>
<Row>
<Col>
<WeatherPanel data={data} handleClick={handleClick} />
</Col>
<Col>{data.length > 0 && <ActiveWeather activeWeather={activeWeather} data={data[0]} />}</Col>
</Row>
</Container>
</div>
);
}
export default App;
when trying to use useEffect...
if (props.activeWeather) {
setWeather(props.activeWeather);
}
is always exectued because an empty array is truthy. So when activeWeather is an empty array, weather is being set to []
Try like this :-
​useEffect(() => {
​if (props.activeWeather) {
​setWeather(props.activeWeather);
​}}, [props.activeWeather])
Set your props activeWeather in useEffect array. if any changes done in your parent component chiled useEffect automatically tigger.

Pass data from API to another component with TypeScript and ReactJS

i'am learning TS yet and I trying to create an application where I get data from API, show results and if someone click on item, it shows a modal with more details, but i'am trouble cause basically my component doesn't render... Look at my code =) !
import IMovie from "../../models/movie.model";
import Modal from "../modal/Modal";
import "./style";
import {
ResultsBody,
ResultsContainer,
TitleResult,
MovieStats,
MovieCover,
MovieStatsDescription,
} from "./style";
interface ISearch {
search?: string;
}
const URL =
"#";
const Results = ({ search }: ISearch) => {
const [data, setData] = React.useState<IMovie[]>([]);
const [currentPage, setCurrentPage] = React.useState(1);
const [dataPerPage] = React.useState(10);
async function getData() {
const response: AxiosResponse<any> = await axios.get(URL);
setData(response.data.results);
}
React.useEffect(() => {
getData();
}, []);
const indexLastData = currentPage * dataPerPage;
const indexFirstData = indexLastData - dataPerPage;
const currentData = data.slice(indexFirstData, indexLastData);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
const filteredData = data.filter((results) => {
return results.title.toLowerCase().includes(search!.toLocaleLowerCase());
});
return (
<>
<ResultsContainer>
<TitleResult>
<span>Personagem</span>
<span>Sinopse</span>
<span>Data</span>
</TitleResult>
{!search
? currentData.map((item) => (
<ResultsBody
key={item.id}
// onClick={() => {
// selectedMovie(item);
// }}
>
<MovieCover
src={`https://image.tmdb.org/t/p/w185${item.poster_path}`}
alt="poster"
/>
<MovieStats style={{ fontWeight: `bold` }}>
{item.title}
</MovieStats>
<MovieStatsDescription>{item.overview}</MovieStatsDescription>
<MovieStats>{item.release_date}</MovieStats>
</ResultsBody>
))
: filteredData.map((item) => (
<ResultsBody key={item.id}>
<MovieCover
src={`https://image.tmdb.org/t/p/w185${item.poster_path}`}
alt="poster"
/>
<MovieStats style={{ fontWeight: `bold` }}>
{item.title}
</MovieStats>
<MovieStatsDescription>{item.overview}</MovieStatsDescription>
<MovieStats>{item.release_date}</MovieStats>
</ResultsBody>
))}
</ResultsContainer>
<Modal data={data} /> //HERE IS WHERE I'AM CALLING MY MODAL, I want to pass data here
<Pagination
dataPerPage={dataPerPage}
totalData={data.length}
paginate={paginate}
currentPage={currentPage}
/>
</>
);
};
export default Results;
This is my MODAL component
import React from "react";
import { ModalContainer } from "./style";
import IMovie from "../../models/movie.model";
interface IData {
data: IMovie[];
}
const Modal = ({ data }: IData) => {
console.log(data);
return <ModalContainer>{data.title}</ModalContainer>; //HERE IS NOT WORKING
};
export default Modal;
As you can see guys, I can show all results on console.log, but when I put inside the return the log says ''TypeError: Cannot read property 'title' of undefined''
If someone could help me I'd really appreciate! Thanks a lot =)
Movie vs Array
You are getting the error
'Property 'title' does not exist on type 'IMovie[]'. TS2339
in your Modal component because data is an array of movies. An array doesn't have a title property.
You want the modal to show one movie, so you should only pass it one movie.
interface IData {
data: IMovie;
}
Current Selection
Changing the IData interface fixes the issues in Modal, but creates a new error in Results because we are still passing an array. The correct prop is the data for the movie that was clicked. What movie is that? We need to use a useState hook in order to store that data.
Depending on where you control the open/closed state of the Modal, you may also want to pass an onClose callback that clears the selected movie state.
the state:
const [selected, setSelected] = React.useState<IMovie | null>(null); // is a movie or null
in the movie:
onClick={() => setSelected(item)}
the modal:
{selected === null || (
<Modal
data={selected}
onClose={() => setSelected(null)}
/>
)}
Avoid Duplicated Code Blocks
You are rendering a movie the same way whether it's from currentData or filteredData, so we want to combine those. We could create a shared renderMovie callback or ResultsMovie component to use in both loops, but I think we can actually handle it higher up and just have one loop.
You also want your pagination to reflect the pages of just the matching movies when we are filtering based on a search.
// the matchingMovies is a filtered array when there is a search, or otherwise the entire array
const matchingMovies = search
? data.filter((result) =>
result.title.toLowerCase().includes(search.toLowerCase())
)
: data;
const indexLastData = currentPage * dataPerPage;
const indexFirstData = indexLastData - dataPerPage;
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// total for the pagination should be based on matchingMovies instead of data
const totalData = matchingMovies.length;
// make the currentData from the matchingMovies
const currentData = matchingMovies.slice(indexFirstData, indexLastData);
There might be some bugs or potential additional improvements but I can't actually run this without your components :)
const Results = ({ search }: ISearch) => {
const [data, setData] = React.useState<IMovie[]>([]);
const [currentPage, setCurrentPage] = React.useState(1);
const [dataPerPage] = React.useState(10);
const [selected, setSelected] = React.useState<IMovie | null>(null); // is a movie or null
async function getData() {
const response: AxiosResponse<any> = await axios.get(URL);
setData(response.data.results);
}
React.useEffect(() => {
getData();
}, []);
// the matchingMovies is a filtered array when there is a search, or otherwise the entire array
const matchingMovies = search
? data.filter((result) =>
result.title.toLowerCase().includes(search.toLowerCase())
)
: data;
const indexLastData = currentPage * dataPerPage;
const indexFirstData = indexLastData - dataPerPage;
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// make the currentData from the matchingMovies
const currentData = matchingMovies.slice(indexFirstData, indexLastData);
return (
<>
<ResultsContainer>
<TitleResult>
<span>Personagem</span>
<span>Sinopse</span>
<span>Data</span>
</TitleResult>
{currentData.map((item) => (
<ResultsBody key={item.id} onClick={() => setSelected(item)}>
<MovieCover
src={`https://image.tmdb.org/t/p/w185${item.poster_path}`}
alt="poster"
/>
<MovieStats style={{ fontWeight: `bold` }}>{item.title}</MovieStats>
<MovieStatsDescription>{item.overview}</MovieStatsDescription>
<MovieStats>{item.release_date}</MovieStats>
</ResultsBody>
))}
</ResultsContainer>
{selected === null || (
<Modal data={selected} onClose={() => setSelected(null)} />
)}
<Pagination
dataPerPage={dataPerPage}
totalData={matchingMovies.length}
paginate={paginate}
currentPage={currentPage}
/>
</>
);
};
interface ModalProps {
data: IMovie;
onClose: () => void;
}
const Modal = ({ data, onClose }: ModalProps) => {
console.log(data);
return <ModalContainer>{data.title}</ModalContainer>;
};

Why useEffect runs every time when component re-render?

In my Home component(I call it Home Page!) I am using Cards.JS component which has posts attribute as shown in following code.
const Home = () => {
const dispatch = useDispatch()
const isLoading = useSelector(state => state.isLoading)
const currentPage = useSelector((state) => state.idFor.currentPageHome)
const homePosts = useSelector((state) => state.posts)
useEffect(() => {
dispatch(setIsLoading(true))
dispatch(getAllPosts(currentPage))
}, [dispatch, currentPage])
return (
isLoading ? (
<Loader type="ThreeDots" color="#000000" height={500} width={80} />
) : (
<Cards posts={homePosts} setCurrentPage={setCurrentPageHome} currentPage={currentPage} pageName={"LATEST"} />
)
)
}
And Cards.Js is as following
const Cards = ({ posts, setCurrentPage, currentPage, pageName }) => {
console.log('Cards.JS called', posts);
const dispatch = useDispatch()
useEffect(() => {
dispatch(setIsLoading(false))
})
const handleNextPage = () => {
dispatch(setIsLoading(true))
dispatch(setCurrentPage(currentPage + 1))
}
const handlePreviousPage = () => {
dispatch(setIsLoading(true))
dispatch(setCurrentPage(currentPage - 1))
}
return (
<div className="container">
<h4 className="page-heading">{pageName}</h4>
<div className="card-container">
{
posts.map(post => <Card key={post._id} post={post} />)
}
</div>
<div className="page-div">
{currentPage !== 1 ? <span className="previous-page" onClick={handlePreviousPage}><</span>
: null}
<span className="next-page" onClick={handleNextPage}>></span>
</div>
</div>
)
}
My Problem:
When i come back to home page useEffect is called everytime and request same data to back-end which are already avaliable in Redux store.
Thanks in Advance :)
useEffect will run every time the component rerenders.
However, useEffect also takes a second parameter: an array of variables to monitor. And it will only run the callback if any variable changes in that array.
If you pass an empty array, it will only run once initially, and never again no matter how many times your component rerenders.
useEffect(() => {
dispatch(setIsLoading(false))
}, [])

Categories