Child Component doesn't rerender when state of parent component changes - reactjs

I have the following issue: I have an Component that renders other components in it. One of this component gets state variables of my parent component as parameter and are using them actively, but they don't rerender when the state of the parent component changes. Another problem that I am facing is that I have an additional item in my list that navigates that is activated when the user has a special roleID. The changing of the state works completely fine, but in this situation the additional item only gets visible after I changed the path param of my url.
parent component:
import React, { useEffect, useState } from 'react';
import {Row, Col} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../../App.css';
import ProfileSettings from './profileSettings';
import SettingsChooser from './settingsChooser';
// import SettingRoutings from '../settingRoutings';
import {BrowserRouter as Router, useHistory, useLocation, useParams} from 'react-router-dom';
// import Routings from '../Routings.js';
import UserRequests from './userRequests';
import useAuth from '../../API/useAuthentification';
import { CONTROLLERS, useBackend } from '../../hooks/useBackend';
function UserSettings({user}) {
const {title: path} = useParams();
const [acst, setAcst] = useState(localStorage.accessToken);
const [rft, setRft] = useState(localStorage.refreshToken);
const history = useHistory();
const [items, setItems] = useState(['Profile', 'Requests','Log Out', 'Delete Account']);
const [authError, setAuthError] = useState(false);
const [userValues, authentificate] = useBackend(authError, setAuthError, user);
const [component, setComponent] = useState(<></>);
const [defaultItem, setDefaultItem] = useState(0);
useEffect(() => {
console.log('render');
authentificate(CONTROLLERS.USERS.getUserByAccessToken());
}, [acst, rft]);
window.addEventListener('storage', () => localStorage.accessToken !== acst ? setAcst(localStorage.accessToken) : '');
window.addEventListener('storage', () => localStorage.refreshToken !== rft ? setRft(localStorage.refreshToken) : '');
useEffect(() => {
if(userValues?.roleID === 1) {
items.splice(0, 0, 'Admin Panel');
setItems(items);
}
console.log(items);
}, [userValues]);
useEffect(() => {
// if(path==='logout') setDefaultItem(2);
// else if(path==='deleteAccount') setDefaultItem(3);
// else if(path==='requests') setDefaultItem(1);
}, [])
const clearTokens = () => {
localStorage.accessToken = undefined;
localStorage.refreshToken = undefined;
}
useEffect(() => {
console.log(path);
if(path ==='logout' && !authError) {
setDefaultItem(2);
clearTokens();
}
else if(path === 'deleteaccount') {
setDefaultItem(3);
if(userValues?.userID && !authError) {
authentificate(CONTROLLERS.USERS.delete(userValues.userID));
}
clearTokens();
history.push('/movies/pages/1');
}
else if(path==='requests') {
setDefaultItem(1);
setComponent(<UserRequests user={userValues} setAuthError={setAuthError} authError={authError}/>);
} else {
setComponent(<ProfileSettings user={userValues} setAuthError={setAuthError} authError={authError}/>);
}
}, [path]);
useEffect(() => {
console.log(defaultItem);
}, [defaultItem])
return (
<div >
<Row className="">
<Col className="formsettings2" md={ {span: 3, offset: 1}}>
<SettingsChooser items={items} headline={'Your Details'} defaultpath='userSettings' defaultactive={defaultItem} />
</Col>
<Col className="ml-5 formsettings2"md={ {span: 6}}>
{authError ? <p>No Access, please Login first</p> : component}
</Col>
</Row>
</div>
);
}
export default UserSettings;
Child component (settingsChooser):
import React, {useEffect, useState} from 'react';
import {Card, Form, Button, Nav, Col} from 'react-bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import { LinkContainer } from 'react-router-bootstrap';
import '../../App.css'
function SettingsChooser({items, headline, defaultpath, defaultactive}) {
const [selected, setSelected] = useState(defaultactive);
const handleClick = (e, key) => {
setSelected(key);
}
useEffect(() => console.log("rerender"), [items, defaultactive]);
useEffect(() => {
setSelected(defaultactive);
}, [])
return(
<>
<Card className="shadow-sm">
<Card.Header className="bg-white h6 ">{headline}</Card.Header>
{items.map((item, idx) =>{
return(
<LinkContainer to={`/${defaultpath}/${(item.replace(/\s/g,'').toLowerCase())}`}><Nav.Link onClick={(e) => handleClick(this, idx)} className={'text-decoration-none text-secondary item-text ' + (selected === idx? 'active-item' : 'item')}>{item}</Nav.Link></LinkContainer>
);
})}
</Card>
</>
);
}
export default SettingsChooser;

Firstly, in your parent component when you do
setItems(items)
you are not actually modifying the state, since items already is stored in the state. React will check the value you pass, and not cause a re-render if the value is already stored in the state. When you modify your array with splice, it is still the "same" array, just different contents.
One way around this is to do setItems([...items]), which will call setItems with a new array, containing the same items.
Secondly, in your child class, the following currently has no effect:
useEffect(() => {
setSelected(defaultactive);
}, [])
Since the dependency array is empty, it will only be called on the first render. If you want it to be called any time defaultactive changes, you need to do this instead:
useEffect(() => {
setSelected(defaultactive);
}, [defaultactive])

Related

infinite scroll, react-intersection-observer, How to add new Array to the foundation Array? (i used spread... but didn't worked)

I'm making movie app (using react.js)
I want to show a list of new movies whenever user scrolls down.
but when i write these codes, it doesn't work.
I used react-intersection-observer and made the second useEffect for adding new list.
can you see what is the problem...?
**import { useInView } from "react-intersection-observer";**
import { useEffect, useRef, useState } from "react";
import Movie from "../components/Movie";
import HeaderComponent from "../components/HomeButton";
import GlobalStyle from "../GlobalStyle";
import { Route, useParams } from "react-router-dom";
import { LoadingStyle, ListContainer } from "../components/styles";
function Home() {
const [loading, setLoading] = useState(true);
const [movies, setMovies] = useState([]);
const [movieSearch, setMovieSearch] = useState("");
const [movieName, setMovieName] = useState("");
const [pageNumber, setPageNumber] = useState(1);
** const { ref, inView } = useInView({
threshold: 0,
});**
const param = useParams();
const getMovies = async () => {
const json = await (
await fetch(
`https://yts.mx/api/v2/list_movies.json?minimum_rating=1&page=${pageNumber}&query_term=${movieName}&sort_by=year`
)
).json();
setMovies(json.data.movies);
setLoading(false);
};
const onChange = event => {
setMovieSearch(event.target.value);
};
const onSubmit = event => {
event.preventDefault();
if (typeof param === Object) {
setMovieName(movieSearch);
getMovies();
} else {
Route(`/main`);
}
};
// When User Searching...
useEffect(() => {
getMovies();
}, [movieName]);
// When User Scroll, Keep Adding Movies at the bottom...
** useEffect(() => {
setPageNumber(prev => prev + 1);
setMovies(prev => {
return [...movies, ...prev];
});
getMovies();
}, [inView]);
**
return (
<>
<GlobalStyle />
<HeaderComponent
onSubmit={onSubmit}
onChange={onChange}
movieSearch={movieSearch}
/>
{loading ? (
<LoadingStyle>Loading...</LoadingStyle>
) : (
<>
<ListContainer>
{movies.map(item => {
return (
<Movie
key={item.title}
id={item.id}
title={item.title}
year={item.year}
medium_cover_image={item.medium_cover_image}
rating={item.rating}
runtime={item.runtime}
genres={item.genres}
summary={item.summary}
/>
);
})}
</ListContainer>
**{inView ? <>๐ŸŽญ</> : <>๐Ÿงถ</>}**
<div ref={ref} style={{ width: "100%", height: "20px" }}></div>
</>
)}
</>
);
}
export default Home;
and, when i debug this code, this errors comes out.
react_devtools_backend.js:4026 Warning: Encountered two children with the same key, `Headless Horseman`. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted โ€” the behavior is unsupported and could change in a future version.
at div
at O (http://localhost:3000/static/js/bundle.js:47415:6)
at Home (http://localhost:3000/static/js/bundle.js:908:80)
at Routes (http://localhost:3000/static/js/bundle.js:41908:5)
at Router (http://localhost:3000/static/js/bundle.js:41841:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:40650:5)
at App

How to use state on one element of map on typescript?

I want to use onClick on one element of my map and set "favorite" for it. Basically, I'm trying to change the SVG of a Icon to the filled version, but with the map, all of items are changing too.
I already try to pass this to onClick, but doesn't work.
My code:
import React, { Component, useState, useEffect } from "react";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { ForwardArrow } from "../../../assets/images/ForwardArrow";
import { BackArrow } from "../../../assets/images/BackArrow";
import * as S from "./styled";
import { IconFavoriteOffer } from "../../../assets/images/IconFavoriteOffer";
import { Rating } from "../../../assets/images/Rating";
import { TruckFill } from "../../../assets/images/TruckFill";
import { OpenBox } from "../../../assets/images/OpenBox";
import { IconCartWht } from "../../../assets/images/IconCartWht";
import axios from "axios";
import { off } from "process";
import SwitcherFavorite from "../SwitcherFavorite";
export default function Carousel() {
const [offers, setOffers] = useState<any[]>([]);
useEffect(() => {
axios.get("http://localhost:5000/offers").then((response) => {
setOffers(response.data);
});
}, []);
const [favorite, setFavorite] = useState(true);
const toggleFavorite = () => {
setFavorite((favorite) => !favorite);
};
return (
<>
<Slider {...settings}>
{offers.map((offer, index) => {
return (
<S.Offer key={index}>
<>
<S.OfferCard>
<S.OfferCardTop>
<S.OfferContentTop>
<S.OfferFavorite>
<S.OfferFavoriteButton onClick={toggleFavorite}> // Want to get this element of mapping
<SwitcherFavorite favorite={favorite} />
</S.OfferFavoriteButton>
</S.OfferFavorite>
<S.OfferStars>
<Rating />
</S.OfferStars>
</S.OfferContentTop>
</S.OfferCardTop>
</S.OfferCard>
</>
</S.Offer>
);
})}
</Slider>
</>
);
}
So, how can I do it?
Instead of using a single boolean flag with your current [favorite, setFavorite] = useState(false) for all the offers, which wouldn't work, you can store the list of offer IDs in an array. In this way you can also have multiple favourited offers.
Assuming your offer item has a unique id property or similar:
// This will store an array of IDs of faved offers
const [favorite, setFavorite] = useState([]);
const toggleFavorite = (id) => {
setFavorite((previousFav) => {
if (previousFav.includes(id)) {
// remove the id from the list
// if it already existed
return previousFav.filter(favedId => favedId !== id);
}
// add the id to the list
// if it has not been here yet
return [...previousFav, id]);
}
};
And then in your JSX:
/* ... */
<S.OfferFavoriteButton onClick={() => toggleFavorite(offer.id) }>
<SwitcherFavorite favorite={favorite.includes(offer.id)} />
// Similar to your original boolean flag to switch icons
</S.OfferFavoriteButton>
/* ... */

How to test if props are being rendered, in circumstances where props are being passed as an object

I'm using React Testing Library to test a cafe review app. I have a parent component CafeList that passes an object containing data about the cafes to a child component Cafe, which renders out the cafe data. The object being passed takes the form { name:name,photoURL:photoURL, id:cafe.id}, and I want to test that the name property is being rendered in Cafes.
I'm having trouble though because I don't know how to test a specific value of an object when using RTL - any suggestions?
Here's the parent component CafeList.jsx
import React, { useState,useEffect } from 'react'
import db from '../fbConfig'
import Cafe from './Cafe'
const CafeList = () => {
const [cafes,setCafe] = useState([])
useEffect(() => {
let cafeArray = []
db.collection('cafes')
.get()
.then(snapshot => {
snapshot.forEach(cafe => {
cafeArray.push(cafe)
})
setCafe(cafeArray)
})
},[])
const [...cafeData] = cafes.map((cafe) => {
const { name, photoURL } = cafe.data()
return { name:name,photoURL:photoURL, id:cafe.id}
})
return(
<div className="cafe-container-container">
<h2 className = 'main-subheading'>Reviews</h2>
<Cafe cafes = {cafeData}/>
</div>
)
}
...and the child component Cafe.jsx
import React from 'react'
import {Link} from 'react-router-dom'
const Cafe = ({ cafes }) => {
return (
<div className="cafe-grid">
{
cafes.map((cafe) => {
return (
<Link
to={`/cafe-reviews/${cafe.id}`}
style={{ textDecoration: "none", color: "#686262" }}
>
<div className="cafe-container">
<h3>{cafe.name}</h3>
<img src={cafe.photoURL}></img>
</div>
</Link>
)
})
}
</div>
)
}
export default Cafe
and lastly, here's the test I wrote
import { render, screen } from '#testing-library/react'
import '#testing-library/jest-dom'
import Cafe from '../components/CafeList'
const testArray = [{name: 'this is the name',photoUrl:'photoURL',id: 'id'}]
test('is cafe name prop being passed ', () =>{
render(<Cafe cafes = {testArray}/>)
const nameElement = screen.getByText(/this is the name/i)
expect(nameElement).toBeInTheDocument()
})

React Hide and Show a Component only using a Start button

I would like to hide two components in the Home component:
DisplayBox and GameBox.
When the user logs in the game starts automatically.
Instead, I'd like to only 'Show' the start button.
Then the user may press the Start button to start the game.
(will eventually have more levels to choose from in the start button component)
import React, { useState, useEffect } from "react";
import "./home.js";
import DisplayBox from '../components/displayBox';
import GameBox from '../components/gameBox/gameBox';
import randomWords from 'random-words'
import "./home.css";
const Home = () => {
const [numLetters, setNumLetters] = useState(5)
const [word, setWord] = useState("")
const [blank, setBlank ] = useState()
console.log("Blank", blank);
console.log("WORD", word)
const getARandomWord = () => {
setWord(randomWords(({ exactly: 1, maxLength: 4, formatter: (word) => word.toUpperCase() })))
}
useEffect(() => {
getARandomWord()
}, [])
function clickStart(){
// return { show: true};
// alert('You Start!');
}
return (
<>
<div>
<button onClick={clickStart} style={{width:"800px"}}>
START
</button>
</div>
<DisplayBox word={word} />
<GameBox numLetters={numLetters} setNumLetters={setNumLetters} word={word} setWord={setWord} getARandomWord={getARandomWord} />
</>
);
};
Home.propTypes = {};
export default Home;
create a new state to oversee whether the game is start or not then:-
Home.js:-
import React, { useState, useEffect } from "react";
import "./home.js";
import DisplayBox from '../components/displayBox';
import GameBox from '../components/gameBox/gameBox';
import randomWords from 'random-words'
import "./home.css";
const Home = () => {
const [numLetters, setNumLetters] = useState(5)
const [word, setWord] = useState("")
const [blank, setBlank ] = useState()
// state to track whether the game is start or not
const [isStart, setIsStart] = useState(false)
console.log("Blank", blank);
console.log("WORD", word)
const getARandomWord = () => {
setWord(randomWords(({ exactly: 1, maxLength: 4, formatter: (word) => word.toUpperCase() })))
}
useEffect(() => {
getARandomWord()
}, [])
// handle game start
function handleGameStart() {
if(isStart) {
// do something when game start
alert('Game starting!!!')
// reset the game again
// setIsStart(false)
} else {
console.log('Game is not starting!')
}
}
// function to oversee what happens after game start
useEffect(() => {
handleGameStart()
}, [isStart])
return (
<>
<div>
<button onClick={() => setIsStart(true)} style={{width:"800px"}}>
START
</button>
</div>
{/* this should only be available when game has started */}
{isStart && (
<>
<DisplayBox word={word} />
<GameBox numLetters={numLetters} setNumLetters={setNumLetters} word={word} setWord={setWord} getARandomWord={getARandomWord} />
</>
)}
</>
);
};
Home.propTypes = {};
export default Home;

On child component state change, update siblings

Partially working example: https://codesandbox.io/s/jolly-smoke-ryb2d
Problem:
When a user expands/opens a component row, all other rows inside the rows parent component need to be collapsed. Unfortunately, I can't seem to get the other sibling rows to collapse.
I tried passing down a handler from the parent to the child that updates the state of the parent which would then in turn propagate down to the children.
Expected Result
On expand/open of a row, collapse any other rows that are open inside the parent component that isn't the one clicked
Code:
App.tsx
import React from "react";
import ReactDOM from "react-dom";
import Rows from "./Rows";
import Row from "./Row";
import "./styles.css";
export interface AppProps {}
const App: React.FC<AppProps> = props => {
return (
<Rows>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
<Row>
<p>Click me</p>
<p>Collapse</p>
</Row>
</Rows>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Rows.tsx
Rows.tsx
import React, { useState, useEffect } from "react";
import Row, { RowProps } from "./Row";
export interface RowsProps {}
const Rows: React.FC<RowsProps> = props => {
const [areRowsHidden, setAreRowsHidden] = useState<boolean>(false);
useEffect(() => {});
const handleOnShow = (): void => {};
const handleOnCollapse = (): void => {};
const renderChildren = (): React.ReactElement[] => {
return React.Children.map(props.children, child => {
const props = Object.assign(
{},
(child as React.ReactElement<RowsProps>).props,
{
onShow: handleOnShow,
onCollapse: handleOnCollapse,
isCollapsed: areRowsHidden
}
);
return React.createElement(Row, props);
});
};
return <>{renderChildren()}</>;
};
export default Rows;
Row.tsx
import React, { useState, useEffect } from "react";
export interface RowProps {
onCollapse?: Function;
onShow?: Function;
isCollapsed?: boolean;
}
const Row: React.FC<RowProps> = props => {
const [isCollapsed, setIsCollapsed] = useState(props.isCollapsed || true);
useEffect(() => {}, [props.isCollapsed]);
const handleClick = (): void => {
if (isCollapsed) {
props.onShow();
setIsCollapsed(false);
} else {
props.onCollapse();
setIsCollapsed(true);
}
};
return (
<>
{React.cloneElement(props.children[0], {
onClick: handleClick
})}
{isCollapsed ? null : React.cloneElement(props.children[1])}
</>
);
};
export default Row;
I would store which row is open inside of Rows.tsx and send that value down to its children rather than having the child control that state. You may see this being referred to as lifting state up. You can read more about it here.
Rows.tsx
import React, { useState } from 'react'
import Row from './Row'
export interface RowsProps {}
const Rows: React.FC<RowsProps> = props => {
const [visibleRowIndex, setVisibleRowIndex] = useState<number>(null)
const renderChildren = (): React.ReactElement[] => {
return React.Children.map(props.children, (child, index) => {
const props = Object.assign({}, (child as React.ReactElement<RowsProps>).props, {
onShow: () => setVisibleRowIndex(index),
onCollapse: () => setVisibleRowIndex(null),
isCollapsed: index !== visibleRowIndex
})
return React.createElement(Row, props)
})
}
return <>{renderChildren()}</>
}
export default Rows
Row.tsx
import React from 'react'
export interface RowProps {
onCollapse?: Function
onShow?: Function
isCollapsed?: boolean
}
const Row: React.FC<RowProps> = props => {
const handleClick = (): void => {
if (props.isCollapsed) {
props.onShow()
} else {
props.onCollapse()
}
}
return (
<>
{React.cloneElement(props.children[0], {
onClick: handleClick
})}
{props.isCollapsed ? null : React.cloneElement(props.children[1])}
</>
)
}
export default Row
Example: https://codesandbox.io/s/gifted-hermann-oz2zw
Just a side note: I noticed you're cloning elements and doing something commonly referred to as prop drilling. You can avoid this by using context if you're interested although not necessary.

Resources