I know this should be stupid simple. All of the examples I can find on here are stupid simple. But for some reason I can't get a className to change conditionally in my component.
I'm trying to animate the site logo when the user scrolls down. fullLogo is changing as expected according to the logs. But the class never changes. The only thing I can think is that once the render happens that's it, and it needs to be declared as a class in order to re-render like that. Is that the issue?
const headerHeight = 89
let fullLogo = true
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
fullLogo = false;
} else {
fullLogo = true;
}
console.log('fullLogo', fullLogo)
})
<header className={(!!fullLogo ? 'full-logo' : '')}>
Here's the full component if that helps:
import React, { useState } from "react"
import PropTypes from "prop-types"
import {
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink
} from 'reactstrap'
import "./header.scss"
const Header = ({ siteTitle, menu, location }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const headerHeight = 89
let fullLogo = true
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
fullLogo = false;
} else {
fullLogo = true;
}
console.log('fullLogo', fullLogo)
})
menu.forEach((item, index) => {
if (item.path === location.pathname) {
item.active = true
} else {
item.active = false
}
})
return (
<header className={(!!fullLogo ? 'full-logo' : '')}>
<Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/">
<span className="initial">L</span>
<span className="rest">OGO</span>
<span className="splitter"></span>
<span className="initial">N</span>
<span className="rest">AME</span>
</NavbarBrand>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="mr-auto" navbar>
{menu.map((item, index) => (
<NavItem key={`nav_link_${index}`}>
<NavLink href={item.path} className={item.active ? `active` : null}>{item.title}</NavLink>
</NavItem>
))}
</Nav>
</Collapse>
</Navbar>
</header>
)
}
Header.propTypes = {
siteTitle: PropTypes.string,
menu: PropTypes.array
}
Header.defaultProps = {
siteTitle: ``,
menu: {
title: `Home`,
path: `/`
}
}
export default Header
The code by using hooks be like
import React, { useState } from "react"
import PropTypes from "prop-types"
import {
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink
} from 'reactstrap'
import "./header.scss"
const Header = ({ siteTitle, menu, location }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const headerHeight = 89
const [fullLogo,setFullLogo] = useState(true)
window.addEventListener('scroll', (e) => {
if (window.pageYOffset > headerHeight) {
console.log('scrolled beyond header height')
setFullLogo(false);
} else {
setFullLogo(true);
}
console.log('fullLogo', fullLogo)
})
menu.forEach((item, index) => {
if (item.path === location.pathname) {
item.active = true
} else {
item.active = false
}
})
return (
<header className={(!fullLogo ? 'full-logo' : '')}> //If You Want to toggle current state
<Navbar color="light" light expand="md" fixed="top">
<NavbarBrand href="/">
<span className="initial">L</span>
<span className="rest">OGO</span>
<span className="splitter"></span>
<span className="initial">N</span>
<span className="rest">AME</span>
</NavbarBrand>
<NavbarToggler onClick={toggle} />
<Collapse isOpen={isOpen} navbar>
<Nav className="mr-auto" navbar>
{menu.map((item, index) => (
<NavItem key={`nav_link_${index}`}>
<NavLink href={item.path} className={item.active ? `active` : null}>{item.title}</NavLink>
</NavItem>
))}
</Nav>
</Collapse>
</Navbar>
</header>
)
}
Header.propTypes = {
siteTitle: PropTypes.string,
menu: PropTypes.array
}
Header.defaultProps = {
siteTitle: ``,
menu: {
title: `Home`,
path: `/`
}
}
export default Header
Your assumption is correct. There's no lifecycle event that's triggering a re-render. Functional components only update if you change props or use hooks. Here you could use a hook to setDisplayFullLogo.
Related
I am experimenting the GSAP tutorial to understand how it works with React. I can't figure out why I cannot click on my navigation link anymore when the following code has been implemented in my home page :
My Circle Component
const Circle = forwardRef(({ size, delay }, ref) => {
const el = useRef();
useImperativeHandle(
ref,
() => {
return {
moveTo(x, y) {
gsap.to(el.current, { x, y, delay });
},
};
},
[delay]
);
return <div className={`circle ${size}`} ref={el}></div>;
});
(extract of) My Homepage
import { gsap } from "gsap";
import { useEffect, useRef } from "react";
import { Link } from "react-router-dom";
import Circle from "../../components/circle/Circle";
const Home = () => {
//special cursor
const circleRefs = useRef([]);
useEffect(() => {
circleRefs.current.forEach((ref) =>
ref.moveTo(window.innerWidth / 2, window.innerHeight / 2)
);
const onMove = ({ clientX, clientY }) => {
circleRefs.current.forEach((ref) => ref.moveTo(clientX, clientY));
};
window.addEventListener("pointermove", onMove);
return () => window.removeEventListener("pointermove", onMove);
}, []);
const addCircleRef = (ref) => {
if (ref) circleRefs.current.push(ref);
};
return (
<div className="home">
<div className="topbar">
<nav>
<ul>
<Link to={`/characters`}>
<li>Characters</li>
</Link>
<Link to={`/comics`}>
<li>Comics</li>
</Link>
<Link to={`/favorites`}>
<li>Favorites</li>
</Link>
</ul>
</nav>
</div>
<Circle size="sm" ref={addCircleRef} delay={0} />
<Circle size="md" ref={addCircleRef} delay={0.1} />
<Circle size="lg" ref={addCircleRef} delay={0.2} />
</div>
);
};
export default Home;
Is it because ref is placed on the cursor ?
Problem statement:
On clicking the react scroll link, the link is not highlighted(I've used spy) and it is not scrolling to the div instead just landing to the page.
Is there any other efficient way to do it? As i'm learning react by doing
Page Context component:
export const PageContext = createContext({
pageId: 'homepage',
scrollToPage: () => {}
})
There is a homepage component
const Home = () => {
const [pageId, setPageId] = useState('homepage');
const anotherCompRef = React.useRef();
const profileCompRef = React.useRef();
const scrollToPage = (event) => {
let pageId = event.target ? event.target.getAttribute('data-pageId') : null
if (pageId) {
setPageId(pageId);
Scroll.scroller.scrollTo(pageId, {
duration: 500,
delay: 100,
smooth: true,
spy: true,
exact: true,
offset: -80,
})
}
}
const renderer = () => {
switch (pageId) {
case 'profile':
return <ProfileView profileCompRef={profileCompRef} />
default:
return <AnotherView anotherCompRef={anotherCompRef}/>
}
}
return (
<>
<PageContext.Provider value={{pageId, scrollToPage: e => scrollToPage(e)}}>
<Layout>
{renderer()}
</Layout>
</PageContext.Provider>
</>
)
}
Layout component:
const Layout = ( {children} ) => {
return (
<>
<Header/>
<MainContainer children={children}/>
<Footer />
</>
)
}
export default Layout
Profileview component:
const ProfileView = (props) => {
return (
<>
<ProfileContainer id='profile' ref={props.profileCompRef} >
do stuff
</ProfileContainer>
</>
)
}
export default ProfileView
AnotherView component
const AnotherView = (props) => {
return (
<>
<AnotherViewContainer id='anotherView' ref={props.anotherCompRef} >
do stuff
</AnotherViewContainer>
</>
)
}
export default AnotherView
Header component:
const Header = () => {
const pageContext = useContext(PageContext)
return (
<>
<NavbarContainer>
<NavbarMenu>
<NavItem>
<NavLink to='profile' data-pageId='profile' smooth={true} duration={500} spy={true} exact='true' offset={-80} onClick={(e) => pageContext.scrollToPage(e)}>
Profile
</NavLink>
</NavItem>
<NavItem>
<NavLink to='anotherView' data-pageId='anotherView' smooth={true} duration={500} spy={true} exact='true' offset={-80} onClick={(e) => pageContext.scrollToPage(e)}>
Another View
</NavLink>
</NavItem>
</NavbarMenu>
</NavbarContainer>
</>
)
}
export default Header
I have mainly fixed the below mentioned issues.
Enable browser scroll by setting the page height more than the viewport height. Only then scrolling can happen.
Not suitable to add a click event to react-scroll Link. So I developed a custom link.
Also did some modifications in updating the pageId also.
Note - Below mentioned only about updated files.
Header.js
import { useContext } from "react";
import PageContext from "./PageContext";
const Header = () => {
const { pageId, setPageId } = useContext(PageContext);
const scrollTo = (e) => {
e.preventDefault();
setPageId(e.target.dataset.pageid);
};
return (
<>
<div>
<div>
<div>
<a
href="#profile"
data-pageid="profile"
onClick={scrollTo}
className={`${pageId === "profile" ? "active" : ""}`}
>
Profile
</a>
</div>
<div>
<a
href="#anotherview"
data-pageid="anotherview"
onClick={scrollTo}
className={`${pageId === "anotherview" ? "active" : ""}`}
>
Another View
</a>
</div>
</div>
</div>
</>
);
};
export default Header;
Home.js
import { useEffect, useRef, useState } from "react";
import { scroller } from "react-scroll";
import AnotherView from "./AnotherView";
import Layout from "./Layout";
import PageContext from "./PageContext";
import ProfileView from "./ProfileView";
const Home = () => {
const [pageId, setPageId] = useState("homepage");
const anotherCompRef = useRef();
const profileCompRef = useRef();
useEffect(() => {
if (pageId !== "homepage")
scroller.scrollTo(pageId, {
duration: 500,
delay: 100,
smooth: true
});
}, [pageId]);
const renderer = () => {
switch (pageId) {
case "profile":
return <ProfileView profileCompRef={profileCompRef} />;
default:
return <AnotherView anotherCompRef={anotherCompRef} />;
}
};
return (
<>
<PageContext.Provider value={{ pageId, setPageId }}>
<Layout>{renderer()}</Layout>
</PageContext.Provider>
</>
);
};
export default Home;
Layout.js
import { useContext } from "react";
import Header from "./Header";
import PageContext from "./PageContext";
const Layout = ({ children }) => {
const { pageId } = useContext(PageContext);
return (
<>
<Header />
<div id={pageId} children={children} />
</>
);
};
export default Layout;
styles.css
#root {
height: calc(100vh + 100px);
}
a.active {
color: red;
}
https://codesandbox.io/s/scrolling-with-react-scroll-68043895-bqt10?file=/src/Home.jsx
Let me know if you need further support.
I've created a React NavBar following a tutorial, when I click the burger menu, the nav expands and collapses as expected, but when I click a link on the nav menu, it goes to the page but the nav bar doesn't collapse. I've checked a few questions/guides but they all link to Bootstrap and this code doesn't use Bootstrap, I'd rather not change the NavBar to Bootstrap if it can be avoided! Any help would be appreciated.
import React, { Component } from "react";
import logo from "../images/logo.svg";
import { FaAlignRight } from "react-icons/fa";
import { Link } from "react-router-dom";
export default class Navbar extends Component {
state = {
isOpen: false
};
handleToggle = () => {
this.setState({ isOpen: !this.state.isOpen });
};
componentDidMount() {
window.addEventListener("scroll", this.resizeHeaderOnScroll);
window.addEventListener("scroll", this.navTransparent);
window.addEventListener("scroll", this.navShadow);
};
resizeHeaderOnScroll() {
const distanceY = window.pageYOffset || document.documentElement.scrollTop,
shrinkOn = 100,
headerEl = document.getElementById("logo");
if (distanceY > shrinkOn) {
headerEl.classList.add("logoShrink");
} else {
headerEl.classList.remove("logoShrink");
}
}
navTransparent() {
const distanceY = window.pageYOffset || document.documentElement.scrollTop,
shrinkOn = 100,
headerEl = document.getElementById("navbar");
if (distanceY > shrinkOn) {
headerEl.classList.add("navbarBg");
} else {
headerEl.classList.remove("navbarBg");
}
}
navShadow() {
const distanceY = window.pageYOffset || document.documentElement.scrollTop,
shrinkOn = 100,
headerEl = document.getElementById("navbar");
if (distanceY > shrinkOn) {
headerEl.classList.add("navShadow");
} else {
headerEl.classList.remove("navShadow");
}
}
render() {
return <nav id="navbar">
<div className="nav-center">
<div className="nav-header">
<Link to="/">
<img id="logo" src={logo} alt="" />
</Link>
<button type="button" className="nav-btn" onClick={this.handleToggle}>
<FaAlignRight className="nav-icon" />
</button>
</div>
<ul className={this.state.isOpen ? "nav-links show-nav" : "nav-links"}>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/nigelservices">Services</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</div>
</nav>;
}
}
To answer the question here you can always do this because the Link component accepts the onClick prop:
export default class Navbar extends Component {
// Rest of your code
handleLinkClick = () => {
this.setState({ isOpen: false });
};
render() {
return (
// Your JSX
<Link to="/" onClick={handleLinkClick}>Home</Link>
)
}
}
Remember to add this in every link component.
As a side note you can also use the NavLink component in react router to handle the styling when the route is the current one. https://reactrouter.com/web/api/NavLink
UPDATE: After some experimentation, I have determined that the problem is that I have some code (see the useEffect() section below) which updates the accordion when the menu object changes. On. the first render, defaultActiveKey works, but on subsequent re-renders, it does not.
I am using the Accordion control from react-bootstrap and can get the basic example from the docs working, but after customizing the code quite a bit, the default accordion no longer opens by... default. Here's probably the most interesting part of the code:
return (
<>
<MenuMobile menuItems={menuItems} open={open} setOpen={setOpen} />
<Navbar bg="white" variant="light" expand="lg" fixed="left">
<Navbar.Brand href="/home">
<img src={logo} width="113" height="40" alt={siteTitle + " Logo"} />
</Navbar.Brand>
<NavbarToggler open={open} setOpen={setOpen} />
<Accordion
defaultActiveKey={menu.defaultActiveKey}
className="sidebar-menu"
data-active={menu.defaultActiveKey}
>
{menu.cards.map((card, index) => {
return (
<Card key={index}>
<CustomToggle title={card.title} eventKey={card.eventKey} anchors={card.anchors} />
<Accordion.Collapse eventKey={card.eventKey}>
<Card.Body>
{card.anchors.map((anchor) => (
<a href={`#${anchor.href}`} key={anchor.href}>
{anchor.text}
</a>
))}
</Card.Body>
</Accordion.Collapse>
</Card>
);
})}
</Accordion>
</Navbar>
</>
);
I've outputted the menu.defaultActiveKey in a data attribute just to make sure it's getting it right and it is. I suspect the problem has to do with the fact that I'm generating the child <Card> components dynamically but I'm not sure what the fix is?
The entire source code is below if you're interested:
import React, { useState, useContext, useEffect } from "react";
import PropTypes from "prop-types";
import Navbar from "react-bootstrap/Navbar";
import AccordionContext from "react-bootstrap/AccordionContext";
import Accordion from "react-bootstrap/Accordion";
import Card from "react-bootstrap/Card";
import { useAccordionToggle } from "react-bootstrap/AccordionToggle";
import classNames from "classnames";
import queryString from "query-string";
import MenuMobile from "./menuMobile";
import NavbarToggler from "./navbarToggler";
import EduMenus from "../utility/educationMenus";
import logo from "../images/logo-white.svg";
const CustomToggle = ({ title, eventKey, anchors, callback }) => {
const currentEventKey = useContext(AccordionContext);
const onClickToggle = useAccordionToggle(eventKey, () => {
callback(eventKey);
});
const isOpen = currentEventKey === eventKey;
return (
<div className={classNames("card-header", { open: isOpen })} onClick={onClickToggle}>
<h2>{title}</h2>
{!!anchors.length && <i className={classNames("fa", { "fa-angle-down": !isOpen }, { "fa-angle-up": isOpen })} />}
</div>
);
};
CustomToggle.propTypes = {
title: PropTypes.string.isRequired,
eventKey: PropTypes.string.isRequired,
anchors: PropTypes.array,
callback: PropTypes.func,
};
CustomToggle.defaultProps = {
anchors: [],
callback: () => null,
};
const DocsNavbar = ({ siteTitle, location }) => {
const [open, setOpen] = useState(false);
const [menu, setMenu] = useState(EduMenus.default);
const menuItems = [
{
href: "/education/overview",
text: "Education",
},
{
href: "/home",
text: "Business",
},
{
href: "/home",
text: "Travel",
},
{
href: "/home",
text: "Healthcare",
},
];
useEffect(() => {
if (!!location && location.search !== "") {
const params = queryString.parse(location.search);
if (params.menu) {
if (Object.prototype.hasOwnProperty.call(EduMenus, params.menu)) {
setMenu(EduMenus[params.menu]);
} else {
console.error(`Menu named '${params.menu}' does not exist`);
}
}
}
});
return (
<>
<MenuMobile menuItems={menuItems} open={open} setOpen={setOpen} />
<Navbar bg="white" variant="light" expand="lg" fixed="left">
<Navbar.Brand href="/home">
<img src={logo} width="113" height="40" alt={siteTitle + " Logo"} />
</Navbar.Brand>
<NavbarToggler open={open} setOpen={setOpen} />
<Accordion
defaultActiveKey={menu.defaultActiveKey}
className="sidebar-menu"
data-active={menu.defaultActiveKey}
>
{menu.cards.map((card, index) => {
return (
<Card key={index}>
<CustomToggle title={card.title} eventKey={card.eventKey} anchors={card.anchors} />
<Accordion.Collapse eventKey={card.eventKey}>
<Card.Body>
{card.anchors.map((anchor) => (
<a href={`#${anchor.href}`} key={anchor.href}>
{anchor.text}
</a>
))}
</Card.Body>
</Accordion.Collapse>
</Card>
);
})}
</Accordion>
</Navbar>
</>
);
};
DocsNavbar.propTypes = {
siteTitle: PropTypes.string,
location: PropTypes.object,
};
DocsNavbar.defaultProps = {
siteTitle: ``,
};
export default DocsNavbar;
Ok, so I came up with a workaround. Not sure if this is the best way but it works so I'll put it out there but if someone has a better solution I'm all ears.
Basically, I removed defaultActiveKey because it seems like it only works on initial render, and explicitly set the active accordion with activeKey and maintain that in state and set that state whenever the menu changes.
import React, { useState, useContext, useEffect } from "react";
import PropTypes from "prop-types";
import Navbar from "react-bootstrap/Navbar";
import AccordionContext from "react-bootstrap/AccordionContext";
import Accordion from "react-bootstrap/Accordion";
import Card from "react-bootstrap/Card";
import { useAccordionToggle } from "react-bootstrap/AccordionToggle";
import classNames from "classnames";
import queryString from "query-string";
import MenuMobile from "./menuMobile";
import NavbarToggler from "./navbarToggler";
import EduMenus from "../utility/educationMenus";
import logo from "../images/logo-white.svg";
const CustomToggle = ({ title, eventKey, anchors, callback }) => {
const currentEventKey = useContext(AccordionContext);
const onClickToggle = useAccordionToggle(eventKey, () => {
callback(eventKey);
});
const isOpen = currentEventKey === eventKey;
return (
<div className={classNames("card-header", { open: isOpen })} onClick={onClickToggle}>
<h2>{title}</h2>
{!!anchors.length && <i className={classNames("fa", { "fa-angle-down": !isOpen }, { "fa-angle-up": isOpen })} />}
</div>
);
};
CustomToggle.propTypes = {
title: PropTypes.string.isRequired,
eventKey: PropTypes.string.isRequired,
anchors: PropTypes.array,
callback: PropTypes.func,
};
CustomToggle.defaultProps = {
anchors: [],
callback: () => null,
};
const DocsNavbar = ({ siteTitle, location }) => {
const [open, setOpen] = useState(false);
const [menu, setMenu] = useState(EduMenus.default);
const [active, setActive] = useState(EduMenus.default.defaultActiveKey);
const menuItems = [
{
href: "/education/overview",
text: "Education",
},
{
href: "/home",
text: "Business",
},
{
href: "/home",
text: "Travel",
},
{
href: "/home",
text: "Healthcare",
},
];
useEffect(() => {
if (!!location && location.search !== "") {
const params = queryString.parse(location.search);
if (params.menu) {
if (Object.prototype.hasOwnProperty.call(EduMenus, params.menu)) {
setMenu(EduMenus[params.menu]);
setActive(EduMenus[params.menu].defaultActiveKey);
} else {
console.error(`Menu named '${params.menu}' does not exist`);
}
}
}
});
return (
<>
<MenuMobile menuItems={menuItems} open={open} setOpen={setOpen} />
<Navbar bg="white" variant="light" expand="lg" fixed="left">
<Navbar.Brand href="/home">
<img src={logo} width="113" height="40" alt={siteTitle + " Logo"} />
</Navbar.Brand>
<NavbarToggler open={open} setOpen={setOpen} />
<Accordion activeKey={active} className="sidebar-menu" onSelect={(e) => setActive(e)}>
{menu.cards.map((card, index) => {
return (
<Card key={index}>
<CustomToggle title={card.title} eventKey={card.eventKey} anchors={card.anchors} />
<Accordion.Collapse eventKey={card.eventKey}>
<Card.Body>
{card.anchors.map((anchor) => (
<a href={`#${anchor.href}`} key={anchor.href}>
{anchor.text}
</a>
))}
</Card.Body>
</Accordion.Collapse>
</Card>
);
})}
</Accordion>
</Navbar>
</>
);
};
DocsNavbar.propTypes = {
siteTitle: PropTypes.string,
location: PropTypes.object,
};
DocsNavbar.defaultProps = {
siteTitle: ``,
};
export default DocsNavbar;
I'm making a simple shopping cart app using Redux. Right now every time I change page, actions are automatically get called for three times which is equal to the number of items. If I go to Cart page, removeItems action gets called three times so there's no way I can add items to cart so far. It might be a problem about router, but I can't spot the problem. Could anyone explain me what is the problem?
Home.js
import React from 'react';
import { connect } from 'react-redux';
import { addToCart } from '../actions';
class Home extends React.Component {
handleClick = id => {
this.props.addToCart(id)
}
renderList = () => {
return this.props.cart.slice(0, 3).map(item => {
return (
<div className="card" key={item.id} style={{width: "200px", float: "left", marginRight: "20px"}}>
<div className="card-image">
<img src={item.imageUrl} alt={item.name} />
<span className="card-title">{item.name}</span>
<span to="/"
className="btn-floating halfway-fab waves-effect waves-light red"
onClick={this.handleClick(item.id)}
>
<i className="material-icons">add</i>
</span>
</div>
<div className="card-content">
<p>{item.desc}</p>
<p><b>${item.price}</b></p>
</div>
</div>
)
})
}
render() {
console.log(this.props.cart)
return (
<div className="container">
<h3>Home</h3>
<div className="box">
{this.renderList()}
</div>
</div>
)
}
}
const mapStateToProps = state => {
return { cart: state.cart.items }
}
const mapStateToDispatch = dispatch => {
return {
addToCart: (id) => { dispatch(addToCart(id)) }
}
}
export default connect(mapStateToProps, mapStateToDispatch)(Home);
Cart.js
import React from 'react';
import { connect } from 'react-redux';
import { removeItem } from '../actions';
class Cart extends React.Component {
handleClick = (id) => {
this.props.removeItem(id);
}
renderList = () => {
if (this.props.addedItems.length !== 0) {
return this.props.addedItems.map(item => {
return (
<li className="collection-item avatar" key={item.id}>
<div className="item-img">
<img src={item.imageUrl} alt={item.name} style={{width: "120px"}} />
</div>
<div className="item-desc">
<span className="title">{item.name}</span>
<p>{item.content}</p>
<p><b>${item.price}</b></p>
</div>
<button
className="waves-effect waves-light btn pink remove"
onClick={this.handleClick(item.id)}
>Remove</button>
</li>
)
})
}
else {
return <p>Nothing is in cart.</p>
}
}
render() {
return (
<div className="container">
<div className="cart">
<ul className="collection">
{this.renderList()}
</ul>
</div>
</div>
)
}
}
const mapStateToProps = state => {
return { addedItems: state.cart.addedItems }
}
const mapDispatchToProps = dispatch => {
return {
removeItem: (id) => {dispatch(removeItem(id))}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cart);
Header.js
import React from 'react';
import { Link } from 'react-router-dom';
const Header = () => {
return (
<nav className="nav-wrapper">
<div className="container">
<Link to="/" className="brand-logo">Shopping</Link>
<ul className="right">
<li><Link to="/">Shop</Link></li>
<li><Link to="/cart">Cart</Link></li>
<li><Link to="/cart"><i className="material-icons">shopping_cart</i></Link></li>
</ul>
</div>
</nav>
)
}
export default Header;
Reducers
import data from '../data.json';
import { ADD_TO_CART, REMOVE_FROM_CART } from "../actions/types";
const INITIAL_DATA = {
items: data,
addedItems: [],
total: 0
}
const cartReducer = (state = INITIAL_DATA, action) => {
switch(action.type) {
case ADD_TO_CART:
let addedItem = state.items.find(item => item.id === action.id);
let existedItem = state.addedItems.find(item => action.id ===item.id);
if (existedItem) {
addedItem.quantity += 1;
return {
...state,
total: state.total + addedItem.price
}
}
else {
addedItem.quantity = 1;
let newTotal = state.total + addedItem.price;
return {
...state,
addedItems: [...state.addedItems, addedItem],
total: newTotal
}
}
case REMOVE_FROM_CART:
let itemToRemove = state.addedItems.find(item => action.id === item.id);
let newItems = state.addedItems.filter(item => action.id !== item.id);
let newTotal = state.total - itemToRemove.price;
return {
...state,
addedItems: newItems,
total: newTotal
}
default:
return state;
}
}
export default cartReducer;
Actions
import { ADD_TO_CART, REMOVE_FROM_CART } from "./types";
export const addToCart = (id) => {
return {
type: ADD_TO_CART,
id
}
}
export const removeItem = (id) => {
return {
type: REMOVE_FROM_CART,
id
}
}
App.js
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Header from './Header';
import Home from './Home';
import Cart from './Cart';
const App = () => {
return (
<BrowserRouter>
<div className="app">
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/cart" component={Cart} />
</Switch>
</div>
</BrowserRouter>
)
}
export default App;
Seems like you're actually calling your click handlers whenever you render your components, instead of just passing the handler function, so that's why actions are being triggered multiple times.
For instance, in your Home.js component change the code below from:
<span to="/" className="btn-floating halfway-fab waves-effect waves-light red" onClick={this.handleClick(item.id)} >
to:
<span to="/" className="btn-floating halfway-fab waves-effect waves-light red" onClick={() => { this.handleClick(item.id); }} >
And the same thing on Cart.js, change from:
<button className="waves-effect waves-light btn pink remove" onClick={this.handleClick(item.id)}>Remove</button>
to:
<button className="waves-effect waves-light btn pink remove" onClick={() => {this.handleClick(item.id); }}>Remove</button>