Problems with React.cloneElement - reactjs

I am trying to create a reusable carousel, where carousel items are scrollable and clickable. Carousel item can be any component which is passed as children to Carousel. The problem is that when I try to pass selected state as prop in the child component using React.cloneElement I get this error react docs Unknown prop warning. There are several answers in stack overflow like this that inspired my solutionand this that exposes the problembut with no good answers specific to my situation. My code so far is this:
import React, { useState } from 'react';
import CarouselItem from './CarouselItem';
type Props = {
children: Array<React.FC>;
};
const Carousel: React.FC<Props> = ({ children }: Props) => {
const [selected, setSelected] = useState(0);
const handleSelect: (index: number) => void = (index: number) => {
setSelected(index);
};
console.log('children', children);
return (
<div className="flex w-full items-center justify-start overflow-y-scroll">
{children.map((child, index) => (
<CarouselItem
key={`carousel-item${index + 1}`}
selected={selected}
index={index}
child={child}
handleSelect={handleSelect}
/>
))}
</div>
);
};
export default Carousel;
import Link from 'next/link';
import React, { useState, useEffect, createContext } from 'react';
interface Props {
selected: number;
index: number;
child: JSX.Element;
handleSelect: (index: number) => void;
}
const CarouselItem: React.FC<Props> = (props) => {
const a = 1;
// props.child.props.children.props.isSelected = 'true';
console.log('typeof props.child', typeof props.child.type);
return (
<li
onClick={() => props.handleSelect(props.index)}
className={`${
props.selected == props.index ? 'selected' : 'unselected'
} list-none`}
>
{React.cloneElement(props.child, {
isSelected: 'true'
})}
</li>
);
};
export default CarouselItem;
const LiveSport: NextPage = () => {
console.log();
return (
<>
<div className="hidden">
<SportsSvgIcons />
</div>
<Carousel>
{carouselMockData.map((itemData, index) => (
<div key={`sport-carousel-${index + 1}`}>
<CarouselSportItem {...itemData} />
</div>
))}
</Carousel>
</>
);
};
export default LiveSport;
The error comes from here:
const CarouselSportItem: React.FC<CarouselSportItemData> = (props) => {
console.log('props', props);
const divProps = Object.assign({}, props);
delete divProps.isSelected;
return (
<div
className={``}
>
<Link href={`${props.sportCode}`}>
<div>
<div>
<div>{`${props.name}`}</div>
</div>
<div>
<div>
{props.isSelected}
</div>
</div>
</div>
</Link>
</div>
);
};
export default CarouselSportItem;
props.isSelected is not there, and I get this error message:
Warning: React does not recognize the isSelected prop on a DOM
element. If you intentionally want it to appear in the DOM as a custom
attribute, spell it as lowercase isselected instead. If you
accidentally passed it from a parent component, remove it from the DOM
element.

Related

React - pass context to SweetAlert popup

My context is as follows:
import React, {createContext, useEffect, useState} from "react";
export const CartContext = createContext();
const CartContextProvider = (props) => {
const [cart, setCart] = useState(JSON.parse(localStorage.getItem('cart')) || []);
useEffect(() => {
localStorage.setItem('cart', JSON.stringify(cart));
}, [cart]);
const updateCart = (productId, op) => {
let updatedCart = [...cart];
if (updatedCart.find(item => item.id === productId)) {
let objIndex = updatedCart.findIndex((item => item.id === productId));
if (op === '-' && updatedCart[objIndex].qty > 1) {
updatedCart[objIndex].qty -= 1;
} else if (op === '+') {
updatedCart[objIndex].qty += 1;
}
} else {
updatedCart.push({id: productId, qty: 1})
}
setCart(updatedCart);
}
const removeItem = (id) => {
setCart(cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{cart, updateCart, removeItem}}>
{props.children}
</CartContext.Provider>
)
};
export default CartContextProvider;
App.js:
import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import NavigationBar from "./components/layout/navigationBar/NavigationBar";
import Homepage from "./pages/homepage/Homepage";
import AboutUsPage from "./pages/aboutUs/AboutUsPage";
import ContactPage from "./pages/contact/ContactPage";
import SearchPage from "./pages/search/SearchPage";
import ShoppingCart from "./components/layout/shoppingCart/ShoppingCart";
import CartContextProvider from "./context/CartContext";
function App() {
return (
<div>
<CartContextProvider>
<Router>
<NavigationBar/>
<ShoppingCart/>
<Routes>
<Route exact path="/" element={<Homepage/>}/>
<Route path="/a-propos" element={<AboutUsPage/>} />
<Route path="/contact" element={<ContactPage/>}/>
<Route path="/recherche" element={<SearchPage/>}/>
</Routes>
</Router>
</CartContextProvider>
</div>
);
}
export default App;
In the component ShoppingCart I am using another component ShoppingCartQuantity which in turn makes use of the context. It works as it should.
Here's the ShoppingCartQuantity component:
import React, {useContext} from "react";
import {CartContext} from "../../../context/CartContext";
import styles from './ShoppingCartQuantity.module.css'
const ShoppingCartQuantity = ({productId}) => {
const {cart, updateCart} = useContext(CartContext);
let qty = 0;
if (cart.find((item => item.id === productId))) {
let objIndex = cart.findIndex((item => item.id === productId));
qty = cart[objIndex].qty;
}
return (
<div>
<span>
<span className={`${styles.op} ${styles.decrementBtn}`} onClick={() => updateCart(productId, '-')}>-</span>
<span className={styles.qty}>{qty}</span>
<span className={`${styles.op} ${styles.incrementBtn}`} onClick={() => updateCart(productId, '+')}>+</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
Now I am trying to use the ShoppingCartQuantity component in the Homepage component which is a route element (refer to App.js) but getting the error Uncaught TypeError: Cannot destructure property 'cart' of '(0 , react__WEBPACK_IMPORTED_MODULE_0__.useContext)(...)' as it is undefined.
So the context is working for components outside the router but not for those inside it. If I have wrapped the router within the provider, shouldn't all the route elements get access to the context or am I missing something?
UPDATE
As user Build Though suggested in the comments, I tried using the ShoppingCartQuantity component in another route element and it works fine; so the problem is not with the router!
Below is the code of how I am using the ShoppingCartQuantity component in the Homepage component:
import React, { useState, useEffect, useRef } from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import Subcat from "../../components/subcat/Subcat";
import CategoryService from "../../services/api/Category";
import SubCategoryService from "../../services/api/SubCategory";
import CategoriesLayout from "../../utils/CategoriesLayout";
import CategoryCard from "../../components/category/CategoryCard";
import { Triangle } from 'react-loader-spinner'
import ScrollIntoView from 'react-scroll-into-view'
import ProductService from "../../services/api/Product";
import Swal from 'sweetalert2'
import withReactContent from 'sweetalert2-react-content';
import YouTube from 'react-youtube';
import FavoriteBtn from "../../components/favorite/FavoriteBtn";
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
import "./Homepage.css";
import "../../components/product/ProductModal.css"
import "react-loader-spinner";
import modalStyles from "../../components/product/ProductModal.module.css"
function Homepage() {
const [categories, setCategories] = useState([]);
const [subCats, setSubCats] = useState([]);
const [loader, setLoader] = useState(false);
const ResponsiveGridLayout = WidthProvider(Responsive);
const scrollRef = useRef();
const productModal = withReactContent(Swal);
const opts = {
// height: '390',
// width: '640',
playerVars: {
autoplay: 1,
}
};
useEffect(() => {
CategoryService.get().then((response) => {
setCategories(response);
});
}, []);
function showSubCatsHandler(catId) {
setLoader(true);
setSubCats([]);
SubCategoryService.get(catId).then((response) => {
setSubCats(response.data);
setLoader(false);
scrollRef.current.scrollIntoView({ behavior: "smooth" });
});
}
function showProductPopupHandler(productId) {
ProductService.get(productId).then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
<h3 className={modalStyles.header}>{product.AMP_Title}</h3>
<h4 className={`${modalStyles.price} ${modalStyles.header}`}>{"CHf " + product.AMP_Price}</h4>
<img className={modalStyles.image} src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/products/' + product.AMP_Image} />
{
product.descriptions.map((desc, _) => (
<div key={desc.AMPD_GUID}>
{
desc.AMPD_Title === '1' && <h4 className={modalStyles.header}>{product.AMP_Title}</h4>
}
{
desc.AMPD_Image !== '' && <img src={process.env.REACT_APP_BACKEND_BASE_URL + 'images/descriptions/' + desc.AMPD_Image} className={desc.AMPD_Alignment === 'left' ? modalStyles.descImageLeft : modalStyles.descImageRight} />
}
<p className={modalStyles.description}>{desc.AMPD_Description}</p>
</div>
))
}
<br/>
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity productId={product.AMP_GUID} />
</div>
<br/>
{
product.AMP_VideoId !== '' &&
<YouTube
videoId={product.AMP_VideoId}
opts={opts}
/>
}
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (
<div>
<div className="categories-container">
<ResponsiveGridLayout
className="layout"
layouts={ CategoriesLayout }
breakpoints={ { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 } }
cols={ { lg: 8, md: 8, sm: 6, xs: 4, xxs: 2 } }
isDraggable={ false }
>
{
categories.map((cat, index) => (
<div key={index}>
<CategoryCard
category_id = {cat.AMC_GUID}
image = {cat.AMC_Image}
showSubCatsHandler = {showSubCatsHandler}
/>
</div>
))
}
</ResponsiveGridLayout>
{
loader &&
<Triangle
height="100"
width="100"
color='#bcad70'
ariaLabel='loading'
wrapperClass="loader"
/>
}
<div ref={scrollRef}>
{
Object.keys(subCats).map((keyName, _) => (
<Subcat
key={subCats[keyName].AMSC_GUID}
title={ subCats[keyName].AMSC_Title }
products={ subCats[keyName].products }
showProductPopupHandler = {showProductPopupHandler}
/>
))
}
</div>
</div>
</div>
);
}
export default Homepage;
I am using the component in a SweetAlert popup. I guess it's the SweetAlert component that is not getting access to the context. Does anyone have an idea how to pass the context to the SweetAlert component?
UPDATE 2
The accepted solution works great except for 1 small issue: the ShoppingCartQuantity component was not re-rendering inside the SweetAlert popup and the qty would not change visually.
I updated the component by using the qty as a state.
const ShoppingCartQuantity = ({ qty, productId, updateCart }) => {
const [quantity, setQuantity] = useState(qty);
const updateCartHandler = (productId, amount) => {
updateCart(productId, amount);
setQuantity(Math.max(quantity + amount, 1));
}
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCartHandler(productId, -1)}
>
-
</span>
<span className={styles.qty}>{quantity}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCartHandler(productId, 1)}
>
+
</span>
</span>
</div>
)
}
Issue
It's very likely that the sweet alert component is rendered outside your app, and thus, outside the CartContextProvider provider. I just searched the repo docs if there is a way to specify a root element, but this doesn't seem possible since this sweet alert code isn't React specific.
See this other similar issue regarding accessing a Redux context in the alert.
Solution
It doesn't seem possible ATM to access the context value from within the modal, so IMHO a workaround could be to refactor your ShoppingCartQuantity component into a wrapper container component to access the context and a presentation component to receive the context values and any callbacks.
I suggest also just passing the amount you want to increment/decrement the quantity by to updateCart instead of passing a "+"/"-" string and operator comparison.
Example:
export const withShoppingCartContext = Component => props => {
const { cart, removeItem, updateCart } = useContext(CartContext);
return <Component {...props} {...{ cart, removeItem, updateCart }} />;
}
const ShoppingCartQuantity = ({ cart, productId, updateCart }) => {
const qty = cart.find(item => item.id === productId)?.qty ?? 0;
return (
<div>
<span>
<span
className={`${styles.op} ${styles.decrementBtn}`}
onClick={() => updateCart(productId, -1)}
>
-
</span>
<span className={styles.qty}>{qty}</span>
<span
className={`${styles.op} ${styles.incrementBtn}`}
onClick={() => updateCart(productId, 1)}
>
+
</span>
</span>
</div>
)
}
export default ShoppingCartQuantity;
In places in your app where ShoppingCartQuantity component is used within the CartContextProvider decorate it with the withShoppingCartContext HOC and use normally.
ShoppingCart
import ShoppingCartQuantityBase, {
withShoppingCartContext
} from "../../components/layout/shoppingCart/ShoppingCartQuantity";
const ShoppingCartQuantity = withShoppingCartContext(ShoppingCartQuantityBase);
const ShoppingCart = (props) => {
...
return (
...
<ShoppingCartQuantity productId={....} />
...
);
};
In places where ShoppingCartQuantity component is used outside the context, like in the sweet modal, access the context within the React code and pass in the context values and callbacks.
...
import ShoppingCartQuantity from "../../components/layout/shoppingCart/ShoppingCartQuantity";
...
function Homepage() {
...
const { cart, updateCart } = useContext(CartContext);
const productModal = withReactContent(Swal);
...
function showProductPopupHandler(productId) {
ProductService.get(productId)
.then((response) => {
const product = response.data;
return productModal.fire({
html:
<div>
...
<div>
<FavoriteBtn productId={product.AMP_GUID}/>
<ShoppingCartQuantity
productId={product.AMP_GUID}
{...{ cart, updateCart }}
/>
</div>
...
</div>,
showConfirmButton: false,
showCloseButton: true
});
});
}
return (...);
}
export default Homepage;
Additional Issues
Your context provider is mutating state when updating quantities. When updating nested state you should still create a shallow copy of the array elements that are being updated.
Example:
const CartContextProvider = (props) => {
...
const updateCart = (productId, amount) => {
// only update if item in cart
if (cart.some(item => item.id === productId)) {
// use functional state update to update from previous state
// cart.map creates shallow copy of previous state
setCart(cart => cart.map(item => item.id === productId
? {
...item, // copy item being updated into new object reference
qty: Math.max(item.qty + amount, 1), // minimum quantity is 1
}
: item
));
}
}
const removeItem = (id) => {
setCart(cart => cart.filter(item => item.id !== id));
};
return (
<CartContext.Provider value={{ cart, updateCart, removeItem }}>
{props.children}
</CartContext.Provider>
);
};
You did't show where you are using the ShoppingCart component or the ShoppingCartQuantity component.
Anyway, when you declare a route, you must pass the component, not the root element. So, this line:
<Route exact path="/" element={<Homepage/>}/>
must be
<Route exact path="/" component={Homepage}/>

React Typescript: Type issue when passing multiple refs to child component

I have a main component that passes down multiple refs to Panels component like so
import { useRef } from 'react';
import Panels from './Panels';
export const Main = (props) => {
const childRefs = useRef<HTMLDivElement[]>([]);
const sliderRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const carouselRefs = useRef({ childRefs, sliderRef, wrapperRef });
return (
<div>
<Panels
ref={carouselRefs}
children={props.chldren}
currentPanel={props.currentPanel}
/>
</div>
);
};
export default Main;
and inside of the Panels component I have this code:
import React, { FC, ForwardedRef, MutableRefObject, RefObject } from 'react';
export interface IPanels {
children: React.ReactNode[];
currentPanel: number;
ref: MutableRefObject<{
childRefs: MutableRefObject<HTMLDivElement[]>;
sliderRef: RefObject<HTMLDivElement>;
wrapperRef: RefObject<HTMLDivElement>;
}>;
}
export const Panels: FC<IPanels> = React.forwardRef(
(
{ children, currentPanel }: IPanels,
forwardedRef: ForwardedRef<HTMLDivElement>
) => {
const { wrapperRef, childRefs, sliderRef } = forwardedRef.current;
return (
<div ref={wrapperRef}>
<div ref={sliderRef}>
{children &&
children.length &&
children.map((child, index) => {
return (
<div
className="mx-4 mb-12"
ref={(element) => {
return element && childRefs.current.push(element);
}}
key={index}
>
{child}
</div>
);
})}
</div>
</div>
);
}
);
export default Panels;
I can see there is a typescript error over export const Panels that reads:
Type 'ForwardRefExoticComponent<Pick<IPanels, "children" | "currentPanel"> & RefAttributes<HTMLDivElement>>' is not assignable to type 'FC<IPanels>'.
I think I might have the ref type in IPanels incorrectly, although I'm not entirely sure how. Any help would be great, thanks.

How can I use forwardRef in React Component?

Can someone help me? I'm creating a component inside React, and I want to make it more accessible using forwardRef. In my case, I'm making a button and I'm using the button's properties, and a few more I've done to make it more dynamic.
This is a summary of my code.
export interface ButtonProps
extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
children?: React.ReactNode;
loading?: boolean;
}
class Button extends React.Component<ButtonProps> {
render() {
const {
...otherProps
} = this.props;
return (
<button {...(otherProps)}></button>
)
}
}
export default Button;
I tried to start something, but right away it gave an error
const ForwardedElement = React.forwardRef<ButtonProps, HTMLButtonElement> (
(props: ButtonProps, ref) => <Button {...props}/>
)
export default ForwardedElement;
I suggest you to use useImperativeHandle hook
useImperativeHandle customizes the instance value that is exposed to parent components when using ref. Let's visualize that with an example.
Here's a component as search bar
import React, {forwardRef, useImperativeHandle, useRef} from "react";
import {Form} from "reactstrap";
const SearchBar = (props, ref) => {
const buttonRef = useRef<any>();
useImperativeHandle(ref, () => ({
getCurrentValue: () => {
return buttonRef.current ? buttonRef.current["value"] : '';
},
setCurrentValue: (value) => {
if (buttonRef.current) {
buttonRef.current["value"] = value;
}
}
}));
return (
<Form className="p-3 w-100" onSubmit={(e) => props.onSubmitHandler(e)}>
<div className="form-group m-0">
<div className="input-group">
<input
type="text"
className="form-control"
placeholder="Search ..."
aria-label="Word to be searched"
ref={buttonRef}
/>
<div className="input-group-append">
<button className="btn btn-primary" type="submit">
<i className="mdi mdi-magnify" />
</button>
</div>
</div>
</div>
</Form>
);
}
export default forwardRef(SearchBar);
This is the header component in which we call our search bar component
import React, {useEffect, useRef, useState} from 'react';
import SearchBar from '../Form/Search/SearchBar';
import Router from 'next/router';
const Header = () => {
const mobileSearchRef = useRef<any>();
const [search, setSearch] = useState<any>(false);
const codeSearchHandler = (e) => {
e.preventDefault();
setSearch(!search);
if (mobileSearchRef.current) {
if (mobileSearchRef.current.getCurrentValue() == '') {
return;
}
}
Router.push({
pathname: '/search',
query: {
searchTerm: mobileSearchRef.current
? mobileSearchRef.current.getCurrentValue()
: ''
},
});
mobileSearchRef.current.setCurrentValue('');
};
return (
<React.Fragment>
<header id="page-topbar">
<div className="navbar-header">
<div className="d-flex">
<div className="dropdown d-inline-block d-lg-none ms-2">
<button
onClick={() => {
setSearch(!search);
}}
type="button"
className="btn header-item noti-icon mt-2"
id="page-header-search-dropdown"
>
<i className="mdi mdi-magnify" />
</button>
<div
className={
search
? 'dropdown-menu dropdown-menu-lg dropdown-menu-end p-0 show'
: 'dropdown-menu dropdown-menu-lg dropdown-menu-end p-0'
}
aria-labelledby="page-header-search-dropdown"
>
<SearchBar
id="headerSearchBar"
ref={mobileSearchRef}
onSubmitHandler={(e) => codeSearchHandler(e)}
/>
</div>
</div>
</div>
</div>
</header>
</React.Fragment>
);
};
export default Header;
If we look at the header component, we can see that we get the input value of search bar component using mobileSearchRef and getCurrentValue method. We can also set its value using setCurrentValue method.
You have to pass the ref aside the spread props:
export interface ButtonProps
extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
children?: React.ReactNode;
loading?: boolean;
ref?: React.RefObject<HTMLButtonElement>
}
class Button extends React.Component<ButtonProps> {
render() {
const {
...otherProps,
ref
} = this.props;
return (
<button {...otherProps} ref={ref}></button>
)
}
}
export default Button;
const ForwardedElement = React.forwardRef<ButtonProps, HTMLButtonElement> (
(props: ButtonProps, ref) => <Button {...props} ref={ref}/>
)
export default ForwardedElement;
now it should work, see this question

React Hook useState behavior

I don't quietly undestand when the useState sets initial value. In this code example only on first click props and state values in Table component will be the same. Why they will not be equal on a second/third/fourth click?
import React, { useState } from "react";
import "./App.module.css";
import styles from "./App.module.css";
function App() {
const [mode, setMode] = useState({ bo: false, value: "1" });
const onClickHanlder = (value: string) => {
setMode({ bo: true, value });
};
return (
<div className="App">
<div onClick={() => onClickHanlder("2")}>one</div>
<div onClick={() => onClickHanlder("3")}>one</div>
<div onClick={() => onClickHanlder("4")}>one</div>
<div onClick={() => onClickHanlder("25")}>one</div>
<div onClick={() => onClickHanlder("6")}>one</div>
{mode && <Table value={mode.value} />}
</div>
);
}
export default App;
type TablePropsType = {
value: string
};
export const Table: React.FC<TablePropsType> = (props) => {
const [a, setA] = useState(props.value);
return (
<div>
{a}
{props.value}
</div>
);
};
You never set the state in Table, only on initial render you give it the default state. If you want them to be the same you have to also have to set the state in Table:
import React, {useState, useEffect} from 'react';
function App() {
const [mode, setMode] = useState({bo: false, value: '1'})
const onClickHanlder = (value: string) => {
setMode({bo: true, value})
}
return (
<div className="App">
<div onClick={() => onClickHanlder('2')}>one</div>
<div onClick={() => onClickHanlder('3')}>one</div>
<div onClick={() => onClickHanlder('4')}>one</div>
<div onClick={() => onClickHanlder('25')}>one</div>
<div onClick={() => onClickHanlder('6')}>one</div>
{
mode && <Table value={mode.value}/>
}
</div>
);
}
export default App;
type TablePropsType = {
value: string,
}
export const Table:React.FC<TablePropsType> = (props) => {
const [a, setA] = useState(props.value)
useEffect(()=>{
setA(props.value)
},[props.value])
return (
<div>
{a}
{props.value}
</div>
)
}

Counting number of times added with useContext - React and Typescript

I am trying to understand the useContext hook a little bit better. I am playing around with this codesandbox which adds items from the left side component to the right side.
Now, I would like to count the number of times they are added to the list (how many times their resp. add to cart button has been clicked on) and display that next to them.
Do I need to create a completely new context and state hook to have access to the resp. items' values all over?
I tried implementing a counter in the addToItemList(item) function without success. I also tried to implement a counter function outside of it and then implement a global handleClick function in vain as well.
Thanks in advance for any tips and hints!
The code for the list of items:
import { useContext } from "react";
import { data, Item } from "./data";
import { ItemListContext } from "./ItemList.context";
const items: Item[] = data;
export default function ItemList() {
const { itemList, setItemList } = useContext(ItemListContext); // get and set list for context
const addItemToItemList = (item: Item) => {
//you are using the itemList to see if item is already in the itemList
if (!itemList.includes(item)) setItemList((prev) => [...prev, item]);
};
return (
<div className="itemlist">
{items.map((item, index) => (
<div style={{ marginBottom: 15 }} key={index}>
<div style={{ fontWeight: 800 }}>{item.name}</div>
<div>{item.description}</div>
<button onClick={() => addItemToItemList(item)}>
Add to sidebar
</button>
</div>
))}
</div>
);
}
And the code for the container which contains the added items:
import { useContext } from "react";
import { ItemListContext } from "./ItemList.context";
import { Link, useHistory } from "react-router-dom";
export default function ItemContainer() {
const history = useHistory(); //useHistory hooks doc: https://reactrouter.com/web/api/Hooks
const { itemList } = useContext(ItemListContext);
const onNavigate = () => {
history.push("/selectedItemList");
};
return (
<div style={{ flexGrow: 4 }}>
<h1 style={{ textAlign: "center" }}>List of items</h1>
<p>Number of items: {itemList.length}</p>
{itemList.length > 0 && (
<ul>
{itemList.map((item, i) => (
<li key={i}>{item.name}</li>
))}
</ul>
)}
<div>
<button>
{" "}
<Link to="/selectedItemList"> selected list details</Link>
</button>
<div>
<button type="button" onClick={onNavigate}>
selected list with useHistory hook
</button>
</div>
</div>
</div>
);
}
ItemList.context.tsx
import React, {
createContext,
Dispatch,
FunctionComponent,
useState
} from "react";
import { Item } from "./data";
type ItemListContextType = {
itemList: Item[]; // type of your items thata I declare in data.ts
setItemList: Dispatch<React.SetStateAction<Item[]>>; //React setState type
};
export const ItemListContext = createContext<ItemListContextType>(
{} as ItemListContextType
);
export const ItemListContextProvider: FunctionComponent = ({ children }) => {
const [itemList, setItemList] = useState<Item[]>([]);
return (
<ItemListContext.Provider
value={{ itemList: itemList, setItemList: setItemList }}
>
{children}
</ItemListContext.Provider>
);
};
Add wrapper methods to the context.
This assumes names are unique.
type ItemListContextType = {
itemList: Item[]; // type of your items thata I declare in data.ts
addItem: (item: Item) => void;
removeItem: (item: Item) => void;
counter: { [itemName: string]: number };
};
export const ItemListContext = createContext<ItemListContextType>(
{} as ItemListContextType
);
export const ItemListContextProvider: FunctionComponent = ({ children }) => {
const [itemList, setItemList] = useState<Item[]>([]);
const [counter, setCounter] = useState<{ [itemName: string]: number }>({});
const addItem = useCallback((item) => {
setCounter((prev) => ({
...prev,
[item.name]: (prev[item.name] || 0) + 1,
}));
setItemList((prev) => [...prev, item]);
}, []);
const removeItem = useCallback((itemName) =>
setItemList((prev) => prev.filter((it) => it.name !== itemName)), []
);
return (
<ItemListContext.Provider
value={{ itemList: itemList, addItem, removeItem, counter }}
>
{children}
</ItemListContext.Provider>
);
};

Resources