onClick is not working in Styled component in react - reactjs

I am trying to implement a dropdown in react styled component. I am trying to set the submenu onClick. But on click function is not getting called. I want to set submenu on click of SidebarLink component. and display the dropdown submenu
I tried this https://github.com/styled-components/vue-styled-components/issues/22
https://www.geeksforgeeks.org/how-to-create-a-responsive-sidebar-with-dropdown-menu-in-reactjs/
SidebarData
import React from "react";
export const SidebarData = [
{
title: "Feeds",
path: "/feeds",
icon: "Images/images/feed_inactive.svg",
iconsrc: "Images/images/feed_active.svg",
},
{
title: "Groups",
path: "/groups",
icon: "Images/images/notactive_people.svg",
iconsrc: "Images/images/icon_ionic_ios_people.svg",
subNav: [
{
title: "All",
status: "all",
cName: "sub-nav",
},
{
title: "Have met",
status: "met",
cName: "sub-nav",
},
{
title: "Favorite",
status: "fav",
cName: "sub-nav",
},
{
title: "Expired",
status: "expired",
cName: "sub-nav",
},
],
},
{
title: "Chats",
path: "/chats",
icon: "Images/images/notactive_chat_bubble.svg",
iconsrc: "Images/images/icon_material_chat_bubble.svg",
subNav: [
{
title: "Open Threads",
cName: "sub-nav",
status: "threads",
},
{
title: "Favorite",
cName: "sub-nav",
status: "favMem",
},
{
title: "Expired",
cName: "sub-nav",
status: "expChat",
},
],
},
{
title: "Event",
path: "/event",
icon: "Images/images/intersection_51.svg",
iconsrc: "Images/images/intersection_act.svg",
},
];
SidebarLink
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { Image } from "react-bootstrap";
const SidebarLink = styled.button`
display: flex;
color: black;
justify-content: space-between;
align-items: center;
padding: 20px;
list-style: none;
height: 60px;
text-decoration: none;
font-size: 18px;
&:hover {
background: #252831;
border-left: 4px solid green;
cursor: pointer;
}
`;
const SidebarLabel = styled.span`
margin-left: 16px;
`;
const DropdownLink = styled(Link)`
background: #252831;
height: 60px;
padding-left: 3rem;
display: flex;
align-items: center;
text-decoration: none;
color: black;
font-size: 18px;
&:hover {
background: green;
cursor: pointer;
}
`;
const SubMenu = (props) => {
const [subnavigation, setSubnavigation] = useState(false);
const showSubnav = () => {
console.log("ddjhj");
setSubnavigation(!subnavigation);
};
useEffect(() => {
console.log("subnav", subnavigation);
}, [subnavigation]);
return (
<>
<SidebarLink onClick={showSubnav}>
{props.item.title}
<span>
{" "}
{props.item.path === window.location.pathname ? (
<Image src={props.item.iconsrc} />
) : (
<Image src={props.item.icon} />
)}
</span>
</SidebarLink>
{subnavigation &&
props.item.subNav.map((item, index) => {
return (
<DropdownLink onClick={() => props.status(item.status)} key={index}>
{item.icon}
<SidebarLabel>{item.title}</SidebarLabel>
</DropdownLink>
);
})}
</>
);
};
export default SubMenu;
Sidebar
import React, { useState, useContext, useEffect } from "react";
import styled from "styled-components";
import { Link } from "react-router-dom";
import * as FaIcons from "react-icons/fa";
import * as AiIcons from "react-icons/ai";
import { SidebarData } from "./SidebarData";
import SubMenu from "./SubMenu";
import { IconContext } from "react-icons/lib";
import { Image } from "react-bootstrap";
import { useDispatch } from "react-redux";
import { MdAccountBalanceWallet } from "react-icons/md";
import { getUserProfile } from "../../../Redux/Slices/DashboardSlice";
import { getPoints, logout } from "../../../Redux/Slices/SidebarSlice";
import {
sidebarToggle,
statusCheck,
userProfile,
} from "../../../Contexts/context";
import "./sidebar.css";
import Loading from "../../../Functions/Loading";
const NavIcon = styled(Link)`
margin-left: 2rem;
font-size: 2rem;
height: 80px;
display: flex;
justify-content: flex-start;
align-items: center;
`;
const SidebarNav = styled.nav`
width: 250px;
height: 100vh;
display: flex;
justify-content: center;
transition: 350ms;
z-index: 10;
`;
const SidebarWrap = styled.div`
width: 100%;
`;
const Sidebar = (props) => {
const dispatch = useDispatch();
const { sidebarShow, setSidebarShow } = useContext(sidebarToggle);
const [sidebar, setSidebar] = useState(false);
const [loading, setLoading] = useState(false);
const { UserData, setUserData } = useContext(userProfile);
const [Points, setPoints] = useState();
const { Status, setStatus } = useContext(statusCheck);
const statusChecked = (status) => {
switch (status) {
case "expired":
return setStatus({
all: false,
fav: false,
met: false,
expired: true,
});
case "met":
return setStatus({
all: false,
fav: false,
met: true,
expired: false,
});
case "fav":
return setStatus({
all: false,
fav: true,
met: false,
expired: false,
});
case "threads":
return setStatus({
favMem: false,
threads: true,
expChat: false,
});
case "favMem":
return setStatus({
favMem: true,
threads: false,
expChat: false,
});
case "expChat":
return setStatus({
favMem: false,
threads: false,
expChat: true,
});
default:
return setStatus({
all: true,
threads: true,
fav: false,
met: false,
expired: false,
});
}
};
useEffect(() => {
dispatch(getUserProfile()).then((response) =>
setUserData(response.payload && response.payload.userprofile)
);
dispatch(getPoints()).then((response) =>
setPoints(response.payload && response.payload.points)
);
}, []);
const logOut = () => {
setLoading(true);
dispatch(logout()).then((response) => {
setLoading(false);
window.location.replace("/");
});
};
return (
<>
<IconContext.Provider value={{ color: "#fff" }}>
<div
className={
sidebarShow
? " vertical-nav bg-white mt-5"
: "vertical-nav bg-white active mt-5"
}
id="sidebar"
>
<SidebarNav>
<SidebarWrap>
{SidebarData.map((item, index) => {
return <SubMenu item={item} key={index} />;
})}
</SidebarWrap>
</SidebarNav>
</div>
</IconContext.Provider>
</>
);
};
export default Sidebar;

Related

Styled-component stopped working on one page. React

I'm using styled-components on my app. Styling works on home page, and all other pages except one List page. When I open this page all components (a mean tags which i created by styled-components) rendering on page, but styling not working. After I open this page, when i go to another pages which worked correctly before, styles on this pages stop working also. It's come back after page reload, but stop working again after opening List page.
here is List page code:
import React, { useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import styled from 'styled-components'
import {
Footer,
ListFilters,
ListProduct,
Navbar,
SearchFilterItem,
} from '../components'
const ListContainer = styled.div`
width: 100%;
`
const ListWrapper = styled.div`
min-height: 100vh;
width: 93vw;
margin: 0 auto;
padding: 30px 0px;
display: flex;
align-items: center;
opacity: ${(props) => props.o};
`
const ProductsContainer = styled.div`
flex: 5;
align-self: flex-start;
`
const ListHeader = styled.div`
display: flex;
flex-direction: column;
padding: 0px 0px 15px 20px;
`
const ListHeaderTop = styled.div`
display: flex;
justify-content: space-between;
`
const ListHeaderBottom = styled.div`
display: flex;
justify-content: flex-start;
margin-top: 10px;
padding-bottom: 10px;
`
const ListHeadeTitle = styled.h1`
font-size: 17px;
font-weight: 500;
`
const FilterBy = styled.select`
padding: 2px 5px;
width: 180px;
border: 1px solid #ccc;
color: #666;
&:focus {
outline: none;
}
`
const FilterByOption = styled.option``
const Products = styled.div`
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
padding-left: 20px;
`
const List = () => {
const location = useLocation()
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const [products, setProducts] = useState([])
const [filters, setFilters] = useState({
categoryFilter: [],
brandFilter: [],
sizeFilter: [],
genderFilter: [],
})
const [cat, setCat] = useState(null)
useEffect(() => {
if (location?.state?.item) {
setCat({ ...cat, category: location?.state?.item })
} else if (location?.state?.brand) {
setCat({ ...cat, brand: location?.state?.brand })
} else {
setCat(null)
}
}, [])
// console.log(location.state)
const qs =
cat &&
Object.keys(cat)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(cat[key])}`
)
.join('&')
useEffect(() => {
let isMounted = true
const source = axios.CancelToken.source()
const getProducts = async () => {
setLoading(true)
try {
const res = await axios.get(
cat
? `http://localhost:5000/api/products?${qs}`
: `http://localhost:5000/api/products`,
{
cancelToken: source.token,
}
)
if (isMounted) {
setProducts(res.data)
setError(null)
const allCats = [
...new Set(
res.data.map((item) => item.category).flat()
),
]
const allBrands = [
...new Set(res.data.map((item) => item.brand)),
]
const allGenders = [
...new Set(res.data.map((item) => item.gender)),
]
const allSizes = [
...new Set(res.data.map((item) => item.size).flat()),
]
setFilters({
categoryFilter: allCats,
brandFilter: allBrands,
sizeFilter: allSizes,
genderFilter: allGenders,
})
}
} catch (error) {
if (isMounted) {
setError(error.message)
setProducts(null)
}
} finally {
isMounted && setLoading(false)
}
}
getProducts()
const cleanUp = () => {
isMounted = false
source.cancel()
}
return cleanUp
}, [cat, qs])
const onToggleFilter = (categ, id) => {
if (cat && Object.entries(cat).flat().includes(id)) {
setCat(
Object.fromEntries(
Object.entries(cat).filter(([key, value]) => value !== id)
)
)
} else {
if (categ === 'gender') {
setCat({ ...cat, gender: id })
} else if (categ === 'size') {
setCat({ ...cat, size: id })
} else if (categ === 'brand') {
setCat({ ...cat, brand: id })
} else {
setCat({ ...cat, category: id })
}
}
}
const onDeleteFilter = (id) => {
// console.log(id)
setCat(
Object.fromEntries(
Object.entries(cat).filter(([key, value]) => value !== id)
)
)
}
console.log(cat)
return (
<>
<Navbar />
<ListContainer>
<ListWrapper o={loading ? 0.35 : 1}>
{/* {loading ? (
<Spinner />
) : (
<> */}
<ListFilters
cat={cat}
setCat={setCat}
filters={filters}
onToggleFilter={onToggleFilter}
/>
<ProductsContainer>
<ListHeader>
<ListHeaderTop>
<ListHeadeTitle>
{products.length} result
{products.length > 1 ? 's' : null}
{' are listed for your search'}
</ListHeadeTitle>
<FilterBy>
<FilterByOption>Sort By:</FilterByOption>
<FilterByOption value="new">
Newest Arrivals
</FilterByOption>
<FilterByOption value="low">
Price: Low
</FilterByOption>
<FilterByOption value="high">
Price: High
</FilterByOption>
</FilterBy>
</ListHeaderTop>
<ListHeaderBottom>
{cat &&
Object.keys(cat).map((key) => (
<SearchFilterItem
onDeleteFilter={onDeleteFilter}
key={key}
info={cat[key]}
/>
))}
</ListHeaderBottom>
</ListHeader>
<Products>
{products.map((item) => (
<ListProduct key={item._id} {...item} />
))}
</Products>
</ProductsContainer>
{/* </>
)} */}
</ListWrapper>
</ListContainer>
<Footer />
</>
)
}
export default List

Values getting undefined when consuming data using useContext in the child component

It is my first time working with useContext hook and I was trying to consume values with useContext in Navbar.js which I've provided in the parent component, but i'm receiving undefined only. In the Navbar.js when I console log user it showsundefined.
I tried to pass the data as props and it works fine. It only happens when I try with Context API. I think I messed up my context api implementation.
Created a context at AuthContext.js
import {createContext} from 'react'
export const AuthContext = createContext({})
Set values in App.js and wrapped using Provider.
import Login from "./pages/Login";
import Register from "./pages/Register";
import Cart from "./components/Cart/Cart";
import Slider from "./components/Slider";
import Categories from "./components/Categories";
import PopularProducts from "./components/PopularProducts";
import Navbar from "./components/Navbar";
import Checkout from "./components/Cart/Checkout";
import Products from "./components/Products";
import Footer from "./components/Footer";
import {
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
createUserWithEmailAndPassword,
} from "firebase/auth";
import { auth } from "./lib/firebase-config";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { useState, useEffect } from "react";
import { commerce } from "./lib/commerce";
import { AuthContext } from "./context/AuthContext";
const App = () => {
const [products, setProducts] = useState([]);
const [cart, setCart] = useState({});
const [order, setOrder] = useState({});
const [errorMessage, setErrorMessage] = useState("");
const [loginEmail, setLoginEmail] = useState("");
const [loginPassword, setLoginPassword] = useState("");
const [user, setUser] = useState({});
const [registerEmail, setRegisterEmail] = useState("");
const [registerPassword, setRegisterPassword] = useState("");
const [isLoggedIn, setIsLoggedIn] = useState(
localStorage.getItem("isLoggedIn")
);
onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
const loginHandler = async (event) => {
event.preventDefault();
try {
const user = await signInWithEmailAndPassword(
auth,
loginEmail,
loginPassword
);
setIsLoggedIn(true);
localStorage.setItem("isLoggedIn", true);
console.log(auth.currentUser.email);
} catch (error) {
console.log(error.message);
}
};
const registerHandler = async () => {
try {
const user = await createUserWithEmailAndPassword(
auth,
registerEmail,
registerPassword
);
setIsLoggedIn(true);
localStorage.setItem("isLoggedIn", true);
console.log({ registerEmail });
console.log(user);
} catch (error) {
console.log(error.message);
}
};
const logoutHandler = async () => {
await signOut(auth);
setIsLoggedIn(false);
localStorage.clear();
};
const fetchProducts = async () => {
const { data } = await commerce.products.list();
setProducts(data);
};
const fetchCart = async () => {
setCart(await commerce.cart.retrieve());
};
const handleAddToCart = async (productId, quantity) => {
const item = await commerce.cart.add(productId, quantity);
setCart(item.cart);
};
const handleUpdateCartQty = async (lineItemId, quantity) => {
const response = await commerce.cart.update(lineItemId, { quantity });
setCart(response.cart);
};
const handleRemoveFromCart = async (lineItemId) => {
const response = await commerce.cart.remove(lineItemId);
setCart(response.cart);
};
const handleEmptyCart = async () => {
const response = await commerce.cart.empty();
setCart(response.cart);
};
const refreshCart = async () => {
const newCart = await commerce.cart.refresh();
setCart(newCart);
};
const handleCaptureCheckout = async (checkoutTokenId, newOrder) => {
try {
const incomingOrder = await commerce.checkout.capture(
checkoutTokenId,
newOrder
);
setOrder(incomingOrder);
refreshCart();
} catch (error) {
setErrorMessage(error.data.error.message);
console.log(error.data);
}
};
useEffect(() => {
fetchProducts();
fetchCart();
}, []);
return (
<>
<AuthContext.Provider
value={
(user,
logoutHandler,
isLoggedIn) }>
<Navbar/>
</AuthContext.Provider>
</>
);
};
export default App;
Trying to consume data using useContext in Navbar.js
import { Badge } from "#material-ui/core";
import {
Search,
ShoppingCartOutlined,
TranslateOutlined,
} from "#material-ui/icons";
import { useContext } from "react";
import styled from "styled-components";
import { mobile } from "../responsive";
import { Link } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
const Parent = styled.div``;
const Container = styled.div`
height: 70px;
background-color: #b6e7f0;
${mobile({ height: "70px" })}
width: 98.75vw;
position: fixed;
top: 0;
z-index: 4;
`;
const Logo = styled.h4`
font-size: 30px;
color: black;
margin-left: 20px;
${mobile({ fontSize: "30px" })}
`;
const Wrapper = styled.div`
padding: 10px 0px;
display: flex;
justify-content: space-between;
max-width: 98.75vw;
${mobile({ padding: "10px 0px" })}
`;
const Left = styled.div`
display: flex;
align-items: center;
text-decoration: none;
width: 20%;
`;
const Center = styled.div``;
const Right = styled.div`
display: flex;
align-items: center;
margin-right: 20px;
justify-content: flex-end;
text-decoration: none;
${mobile({ flex: 2, justifyContent: "flexEnd" })}
`;
const Language = styled.span`
font-size: 14px;
cursor: pointer;
${mobile({ display: "none" })}
`;
const SearchContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
margin-left: 0px;
margin-top: 10px;
padding: 5 px;
border-radius: 5px;
cursor: pointer;
${mobile({ display: "none" })}
&:focus {
}
`;
const Input = styled.input`
border: none;
&:focus {
outline: none;
}
width: 90%;
height: 100%;
border-radius: 5px;
font-size: 15px;
padding: 5px;
margin-right: 20px;
${mobile({ height: "50%" })}
`;
const MenuItem = styled.div`
margin-left: 25px;
margin-top: 3px;
font-size: 18px;
font-weight: 600;
color: #0a6596e6;
cursor: pointer;
justify-content: space-between;
${mobile({ fontSize: "13px", marginLeft: "10px" })}
`;
const Navbar = () => {
const { user, logoutHandler, isLoggedIn } = useContext(AuthContext);
console.log(user)
return (
<Parent>
<Container>
<Wrapper>
<Left>
<Link to={"/"}>
<Logo>audiobae</Logo>
</Link>
</Left>
<SearchContainer style={{ color: "gray", fontSize: 14 }}>
<Input placeholder="Search for products, brands and more" />
<Search style={{ fontSize: "30px" }} />
</SearchContainer>
<Right>
<Language style={{ marginLeft: "5px", marginTop: "3px" }}>
<TranslateOutlined />
</Language>
{console.log(isLoggedIn)}
{isLoggedIn ? (
<>
<MenuItem>{user?.email}</MenuItem>
<MenuItem onClick={logoutHandler}>Logout</MenuItem>
</>
) : (
<>
<Link to={"/register"} style={{ textDecoration: "none" }}>
<MenuItem>Register </MenuItem>
</Link>
<Link to={"/login"} style={{ textDecoration: "none" }}>
<MenuItem>Login</MenuItem>
</Link>
</>
)}
<MenuItem>
<Link to={"/cart"}>
<Badge badgeContent={cart?.total_items} color="primary">
{console.log(cart)}
<MenuItem>
<ShoppingCartOutlined />
</MenuItem>
</Badge>
</Link>
</MenuItem>
</Right>
</Wrapper>
</Container>
</Parent>
);
};
export default Navbar;
try it
<AuthContext.Provider value={{user,logoutHandler,isLoggedIn}}>
<Navbar/>
</AuthContext.Provider>

React hooks component reloading

I'm building a typescript react app that has a child component called Accordion that when is clicked it is opened. When is opened it renders a table with some data. This accordion is made depending on a group that can be changed with a selector. My problem is that I want that when I change this group by my Accordion component closes if it's opened. I tried to pass a prop to close the Accordion but nothing occurs and I'm starting to be frustrated. How can I reload this component in order for the state to be closed? That's my code:
This is my Accordion component:
import React, { useState, useRef, Fragment, ReactChildren, ReactNode } from "react";
import Chevron from "./Chevron"
interface accordionPropsType {
title: string
children: ReactNode
}
const Accordion = (props: accordionPropsType) => {
const [setActive, setActiveState] = useState("");
const [setHeight, setHeightState] = useState("0px");
const [setRotate, setRotateState] = useState("accordion__icon");
const content = useRef(null);
const toggleAccordion = () => {
setActiveState(setActive === "" ? "active" : "");
setHeightState(setActive === "active" ? "0px" : `${content.current.scrollHeight}px`);
setRotateState(setActive === "active" ? "accordion__icon" : "accordion__icon rotate");
}
return(
<Fragment>
<div className="accordion__section">
<button className={`accordion ${setActive}`} onClick={toggleAccordion}>
<p className="accordion__title">{props.title}</p>
<Chevron className={`${setRotate}`} width={10} color={"#777"} />
</button>
<div
ref={content}
style={{ maxHeight: `${setHeight}` }}
className="accordion__content"
>
{props.children}
</div>
</div>
<style jsx>
{`
/* Style the accordion section */
.accordion__section {
display: flex;
flex-direction: column;
margin: 10px;
}
/* Style the buttons that are used to open and close the accordion panel */
.accordion {
background-color: #eee;
color: #444;
cursor: pointer;
padding: 18px;
display: flex;
align-items: center;
border: none;
outline: none;
transition: background-color 0.6s ease;
}
/* Add a background color to the button if it is clicked on (add the .active class with JS), and when you move the mouse over it (hover) */
.accordion:hover,
.active {
background-color: #ccc;
}
/* Style the accordion content title */
.accordion__title {
font-family: "Open Sans", sans-serif;
font-weight: 600;
font-size: 14px;
text-align: left;
}
/* Style the accordion content panel. Note: hidden by default */
.accordion__content {
background-color: white;
overflow: auto;
transition: max-height 0.6s ease;
margin: 5px;
}
/* Style the accordion content text */
.accordion__text {
font-family: "Open Sans", sans-serif;
font-weight: 400;
font-size: 14px;
padding: 18px;
}
`}
</style>
</Fragment>
);
}
export default Accordion;
And this is the component that calls this child component:
import React, { useState, Fragment, useEffect, FormEvent } from "react"
import Select from "../components/Select"
import Accordion from "../components/Accordion"
import Table from "../components/Table"
interface findingsType {
body: object
header: Array<headersType>
}
interface headersType {
className: string
rows: Array<rowType>
}
interface rowType {
className: string
rowspan: number
colspan: number
text: string
}
const CloudFindingsList = (props) => {
const [groupBy, setGroupBy] = useState<Array<string>>([]);
const [tableData, setTableData] = useState<findingsType>(null);
const headerRows = [] as Array<rowType>
const headers = [{
className: "thead_custom" as string,
rows: headerRows
}] as Array<headersType>
console.log('eee')
const getGroupBy = (event) => {
let distinctGroupsBy = []
let allFindings = {}
props.findings.map(finding => {
let value = finding[event.target.value]
distinctGroupsBy.includes(value) ? '' : distinctGroupsBy.push(value)
})
distinctGroupsBy.map(order => {
allFindings[order] = []
})
props.findings.map(finding => {
let value = finding[event.target.value]
distinctGroupsBy.map(order => {
value == order ? allFindings[order].push(finding) : ''
})
});
setGroupBy(distinctGroupsBy)
console.log(groupBy)
Object.keys(allFindings[distinctGroupsBy[0]][0]).map(value => {
headerRows.push({
className: "" as string,
rowspan: 0 as number,
colspan: 0 as number,
text: value as string
})
})
setTableData({
header: headers,
body: allFindings
} as findingsType)
}
const listFindings =
groupBy.map((group, index) => {
return(
<Accordion title={group} key={index}>
<Table jsonData={tableData.body[group]} header={tableData.header}/>
</Accordion>
)
})
return(
<Fragment>
<Select
id='1'
options={[{"val": "severity", "text": "severity"}, {"val": "account", "text": "account"}, {"val": "service", "text": "service"}]}
placeholder='Group by'
handleChange={getGroupBy as () => {}}
/>
{listFindings}
</Fragment>
);
}
export default CloudFindingsList
You don't have to understand all the code I just want that when I change the selected item in the selector the Accordion is closed again. Does anyone see the solution?
Thanks!
You could try to use a useEffect hook passing the props of the Accordion as change parameter. Every time that prop change you execute the code that changes the value.
useEffect(() => {
// YOUR CODE GOES HERE
}, [props])

react router dom link with params dont allways work

I have written a simple search component with autosuggestion in react, it makes calls to the themoviedb. I am using react-router-dom and have defined a route param like this in app.js:
<Route path="/:id" component={SomeComponent} />
and the search component looks like this:
import React, { Component, Fragment } from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";
import axios from "axios";
const SuggestionsResult = styled.ul`
text-decoration: none;
list-style: none;
text-align: left;
display: flex;
flex-direction: column;
width: 100%;
`;
const ResultItem = styled.li`
border-bottom: 0.0625rem solid hsla(0, 0%, 100%, 0.12);
padding: 10px 0 10px;
padding-left: 2px;
font-size: 1em;
cursor: pointer;
&:hover {
background: hsla(0, 0%, 100%, 0.12);
}
`;
export default class Search extends Component {
state = {
query: "",
results: [],
showSuggestions: false
};
handleInputChange = () => {
this.setState(
{
query: this.search.value
},
() => {
if (this.state.query && this.state.query.length > 1) {
if (this.state.query.length % 2 === 0) {
axios
.get(
`https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&query=${
this.state.query
}&page=1&include_adult=false`
)
.then(({ data }) => {
this.setState({
results: data.results,
showSuggestions: !this.state.showSuggestions
});
});
}
} else if (!this.state.query) {
}
}
);
};
handleSuggestionClick = e => {
this.setState({ showSuggestions: false });
};
render() {
return (
<Fragment>
<input
placeholder="Search for a movie..."
ref={input => (this.search = input)}
onChange={this.handleInputChange}
/>
{this.state.showSuggestions && (
<Suggestions
results={this.state.results}
handleSuggestionClick={this.handleSuggestionClick}
/>
)}
</Fragment>
);
}
}
const Suggestions = ({ results, handleSuggestionClick }) => {
const options = results.map(r => (
<ResultItem key={r.id}>
<Link onClick={handleSuggestionClick} to={`/${r.id}`}>
{r.title}
</Link>
</ResultItem>
));
return <SuggestionsResult>{options}</SuggestionsResult>;
};
My problem is when clicking on the link it changes the url but it stays on the same site. If I dont use react-router-dom Link component and only use a elements it works fine but everthing re-renders.
** Update
My react-router code in app.js
<Router>
<Switch>
<Route exact path="/" component={MoviesList} />
<Route path="/:id" component={MovieDetail} />
</Switch>
</Router>
The Suggestions component doesn't receive the Router prop and hence it causes this issue. Wrap your Suggestions component with withRouter HOC. Also make sure that the Search component is rendered as a child or Router component
import React, { Component, Fragment } from "react";
import { Link, withRouter } from "react-router-dom";
import styled from "styled-components";
import axios from "axios";
const SuggestionsResult = styled.ul`
text-decoration: none;
list-style: none;
text-align: left;
display: flex;
flex-direction: column;
width: 100%;
`;
const ResultItem = styled.li`
border-bottom: 0.0625rem solid hsla(0, 0%, 100%, 0.12);
padding: 10px 0 10px;
padding-left: 2px;
font-size: 1em;
cursor: pointer;
&:hover {
background: hsla(0, 0%, 100%, 0.12);
}
`;
export default class Search extends Component {
state = {
query: "",
results: [],
showSuggestions: false
};
handleInputChange = () => {
this.setState(
{
query: this.search.value
},
() => {
if (this.state.query && this.state.query.length > 1) {
if (this.state.query.length % 2 === 0) {
axios
.get(
`https://api.themoviedb.org/3/search/movie?api_key=${apiKey}&language=en-US&query=${
this.state.query
}&page=1&include_adult=false`
)
.then(({ data }) => {
this.setState({
results: data.results,
showSuggestions: !this.state.showSuggestions
});
});
}
} else if (!this.state.query) {
}
}
);
};
handleSuggestionClick = e => {
this.setState({ showSuggestions: false });
};
render() {
return (
<Fragment>
<input
placeholder="Search for a movie..."
ref={input => (this.search = input)}
onChange={this.handleInputChange}
/>
{this.state.showSuggestions && (
<Suggestions
results={this.state.results}
handleSuggestionClick={this.handleSuggestionClick}
/>
)}
</Fragment>
);
}
}
const Suggestions = withRouter(({ results, handleSuggestionClick }) => {
const options = results.map(r => (
<ResultItem key={r.id}>
<Link onClick={handleSuggestionClick} to={`/${r.id}`}>
{r.title}
</Link>
</ResultItem>
));
return <SuggestionsResult>{options}</SuggestionsResult>;
});

react all list items get re-rendered

I have my state structured like this. It's an object with multiple fields inside of it. For certain purposes, I cannot modify the structure of the state. Here's my component which renders the entire List
import React, { Component } from 'react'
import styled from 'styled-components';
import FoodListItem from '../Food-List-Item'
const Wrapper = styled.div`
width: 300px;
max-width: 300px;
`
const Button = styled.button`
width: 300px;
text-align: center;
padding: 1em;
border: 1px solid #eee;
background-color: #ffffff;
text-transform: uppercase;
font-size: 1em;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.45s ease-in-out, border 0.45s ease-in-out;
:hover {
background-color: #eee;
border: 1px solid black;
}
`
class FoodList extends Component {
state = {
data: {
e5d9d9f5: {
label: 'ice cream',
isDelicious: true,
isHealthy: false,
},
a9ba692b: {
label: 'pizza',
isDelicious: true,
isHealthy: false,
},
ze128a47: {
label: 'spinach',
isDelicious: false,
isHealthy: true,
},
},
}
renderListItems = () => {
const { data } = this.state
return Object.keys(data).map((key) => {
return <FoodListItem
key={key}
{...data[key]}
id={key}
handleDecliousChange={this.handleDecliousChange}
handleHealthyChange={this.handleHealthyChange}
/>
})
}
handleDecliousChange = (id) => {
this.setState(state => ({
data: {
...state.data,
[id]: {
...state.data[id],
isDelicious: !state.data[id].isDelicious
}
}
}))
}
handleHealthyChange = (id) => {
this.setState(state => ({
data: {
...state.data,
[id]: {
...state.data[id],
isHealthy: !state.data[id].isHealthy
}
}
}))
}
handleShowAppState = () => {
console.log(this.state.data)
}
render() {
return (
<Wrapper>
{this.renderListItems()}
<Button type="button" onClick={this.handleShowAppState}>Show App State</Button>
</Wrapper>
)
}
}
export default FoodList;
Here's the component which renders a single list item
import React from 'react'
import styled from 'styled-components'
const Title = styled.p`
text-transform: uppercase;
font-weight: bold;
`
const Item = styled.div`
padding: 1em 1em 1em 0em;
padding-left: ${props => props.isDelicious ? '30px': '0px'}
margin-bottom: 2em;
background-color: ${props => props.isHealthy ? 'green' : 'gray'};
transition: background-color 0.45s ease-in-out, padding-left 0.45s ease-in-out;
color: #ffffff;
`
class FoodListItem extends React.PureComponent {
deliciousFn = () => {
this.props.handleDecliousChange(this.props.id)
}
healthyFn = (id) => {
this.props.handleHealthyChange(this.props.id)
}
render() {
console.log('render called', this.props.label);
const {
id,
label, isDelicious, isHealthy,
handleDecliousChange, handleHealthyChange
} = this.props
return (
<Item isHealthy={isHealthy} isDelicious={isDelicious}>
<Title>{label}</Title>
<div>
<input type="checkbox" checked={isDelicious} onChange={this.deliciousFn} />
<label><code>isDelicious</code></label>
</div>
<div>
<input type="checkbox" checked={isHealthy} onChange={this.healthyFn} />
<label><code>isHealthy</code></label>
</div>
</Item>
)
}
}
export default FoodListItem
Whenever I click on a single list item, it re-renders all of them. Is there a way to avoid this? Ideally, only the row which was clicked on should re-render.
You should implement shouldComponentUpdate to handle component’s output in FoodListItem:
Use shouldComponentUpdate() to let React know if a component’s output
is not affected by the current change in state or props. The default
behavior is to re-render on every state change, and in the vast
majority of cases you should rely on the default behavior.
class FoodListItem extends React.Component {
//...
shouldComponentUpdate(nextProps) {
return (
this.props.isDelicious !== nextProps.isDelicious ||
this.props.isHealthy !== nextProps.isHealthy
)
}
//...
}
Reference: https://reactjs.org/docs/react-component.html#shouldcomponentupdate
Or consider using the PureComponent: https://reactjs.org/docs/react-api.html#reactpurecomponent

Resources