i'm doing a testing for a component, and mock the request using msw
- test endpoint using renderHook all of test passes as expected
- testing component with useQuery using render , it just showing loading data.. like image above
so, i think the data not load correctly because useQuery inside the component cant catch the mock of request
i'm trying to find the way, where result from renderHook used inside a render, and nothing
code.test.js
it("rendering issue page", () => {
render(<IssuesSettingPage />, {
wrapper,
});
const heading = await screen.findByRole("heading");
expect(heading).toBeDefined();
debug();
});
wrapper.js
export function wrapper({ children }) {
const queryClient = createQueryClient();
let portal = document.getElementById("#modal");
if (!portal) {
portal = document.createElement("div");
portal.id = "modal";
document.body.appendChild(portal);
}
return (
<RouterContext.Provider value={createMockRouter({})}>
<QueryClientProvider client={queryClient}>
<RecoilRoot>{children}</RecoilRoot>
</QueryClientProvider>
</RouterContext.Provider>
);
}
issues.jsx
import IssueForm from "#/components/forms/IssueForm";
import Button from "#/components/ui/Button";
import Card from "#/components/ui/Card";
import { Input } from "#/components/ui/form";
import Modal from "#/components/ui/Modal";
import Table from "#/components/ui/Table";
import Dashboard from "#/layouts/Dashboard";
import Head from "next/head";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { IoClose, IoSearch, IoTrash } from "react-icons/io5";
import { useQuery, useMutation } from "#tanstack/react-query";
import issue from "#/services/issue";
function IssuesSettingPage() {
const [isAdd, setIsAdd] = useState(false);
const [editedIssue, setEditedIssue] = useState(false);
const [deletedIssue, setDeletedIssue] = useState(false);
const {
register,
watch,
setValue: setSearchValue,
} = useForm({
defaultValues: { search: "" },
});
const search = watch("search");
const { mutate: deleteIssue } = useMutation(issue.useDelete());
const {
isError: isIssueError,
error,
isSuccess: isIssueSuccess,
data: issueData,
isLoading: isIssueLoading,
} = useQuery(issue.get());
const issues = isIssueSuccess ? issueData : [];
const issuesError = isIssueError ? error : null;
const columns = useMemo(() => {
return [
{
accessorKey: "id",
header: "ID",
},
{
id: "number",
accessorKey: "index",
header: () => <div className="text-sm text-center">No</div>,
cell: (info) => (
<div className="text-sm text-center">{info.row.index + 1}</div>
),
size: 50,
},
{
id: "subject",
accessorKey: "subject",
header: "Issue",
header: () => <div className="text-sm pl-5">Issue</div>,
cell: (info) => <div className="text-sm pl-5">{info.getValue()}</div>,
size: 250,
},
{
id: "description",
accessorKey: "description",
header: () => <div className="text-sm pl-5">Description</div>,
cell: (info) => (
<p className="text-sm pl-5">{info.getValue() || "-"}</p>
),
size: 250,
},
{
id: "action",
accessorKey: "action",
header: () => <div className="text-sm text-center">Action</div>,
cell: ({ row }) => (
<div className="flex justify-center">
<div className="!w-[10rem] flex justify-between">
<Button
outline
className="!w-[40px]"
onClick={() => {
setDeletedIssue(row.original.id);
}}
>
<IoTrash />
</Button>
<Button
className="!w-[100px] text-sm"
onClick={() => {
setEditedIssue(row.original);
}}
>
Edit
</Button>
</div>
</div>
),
size: 100,
},
];
}, []);
return (
<>
<Head>
<title>Config Issues</title>
</Head>
{/* <Dashboard activeMenu="setting/issues"> */}
<h2
role="heading"
className="text-cod-gray text-[28px] leading-[42px] font-bold"
>
Config Issues
</h2>
<div className="flex items-center justify-between mt-5">
{/* search issue */}
<div className="relative w-[300px]">
<span className="absolute w-4 h-4 top-1/2 -translate-y-1/2 left-3 text-bombay">
<IoSearch />
</span>
<Input.Text
id="search"
name="search"
placeholder="Search"
className="!pl-8"
autoComplete="off"
register={register}
/>
{search.length > 0 && (
<button
type="button"
className="absolute w-4 h-4 top-1/2 -translate-y-1/2 right-3 text-bombay"
onClick={() => setSearchValue("search", "")}
>
<IoClose />
</button>
)}
</div>
{/*button add issue */}
<div className="ml-auto mr-10">
<Button data-testid="addButton" onClick={() => setIsAdd(true)}>
Add Issue
</Button>
</div>
</div>
{/* issues table */}
<div className="mt-8">
<Card>
<Table
columns={columns}
data={issues}
filter={search}
pageSize={10}
error={issuesError}
loading={isIssueLoading}
/>
</Card>
</div>
{/* </Dashboard> */}
<Modal
centered
show={isAdd}
className="w-[400px]"
onClose={() => setIsAdd(false)}
title="Create Issue"
>
<IssueForm data-testid="addIssue" />
</Modal>
<Modal
centered
show={Boolean(editedIssue)}
className="w-[400px]"
title="Update Issue"
onClose={() => setEditedIssue(false)}
>
<IssueForm
initialValues={editedIssue}
setEditedIssue={setEditedIssue}
/>
</Modal>
<Modal
centered
show={Boolean(deletedIssue)}
className="w-[350px]"
onClose={() => setDeletedIssue(false)}
title="Delete Issue"
>
<>
<p>Are you sure want to delete {deletedIssue?.label}?</p>
<div className="grid grid-cols-2 gap-4 mt-5">
<Button outline block onClick={() => setDeletedIssue(false)}>
Cancel
</Button>
<Button
block
onClick={() => {
// Delete issue
deleteIssue(deletedIssue);
setDeletedIssue(false);
}}
>
Delete
</Button>
</div>
</>
</Modal>
</>
);
}
export default IssuesSettingPage;
All passes but no data showing when rendering the component
i expect mock server data can show into table
[UPDATE]
i solve this problem by putting component which is render inside BeforeEach
and add promise setTimeout
Related
I made Navigation component with dynamic menu items.
`
import React, { useState } from "react";
import NavMenuItems from "../data/NavMenuItems";
function NavBar() {
const [dropDown, setDropDown] = useState({});
const setDropDownOpen = (name) => {
setDropDown({[name]: true });
};
const setDropDownClose = (name) => {
setDropDown({[name]: false });
};
return (
<div className="flex flex-row my-2 mx-5">
{NavMenuItems.map((menu, index) => (
<>
<div key={menu.item} className="relative flex flex-col mx-1">
<div
className="bg-[#121C24] px-2 h-5 text-white text-sm hover:bg-green-700 hover:text-black hover:cursor-pointer "
onMouseEnter={() => setDropDownOpen(menu.item)}
onMouseLeave={() => setDropDownClose(menu.item)}
>
{menu.item}
</div>
{dropDown[menu.item] && (
<div className="bg-slate-200 absolute top-6 px-4 py-2"
onMouseEnter={() => setDropDownOpen(menu.item)}
onMouseLeave={() => setDropDownClose(menu.item)}
>
{menu.subitems.map((submenu, index) => (
<div key={index}>{submenu}</div>
))}
</div>
)}
</div>
</>
))}
</div>
);
}
export default NavBar;
NavMenuItems.js
`
const NavMenuItems = [
{
item: "Events",
subitems: ["Event1", "Event2", "Event2"],
},
{
item: "Reports",
subitems: ["Reports1", "Reports2", "Reports3"],
},
];
export default NavMenuItems
When i mouseover on tabs, its working fine. but when i move over dropdown sub menu, it closes and cant select anything in submenu items.
Can someone help with this?
So when I click add to cart on the shop page it should add the item to the cartItem' array nothing happens, also the cartItems array appears to be of type "never[]" and I don't know how to fix that in jsx.
Project link on GitHub
cart.context.jsx file
import { useState, createContext } from "react";
export const addCartItem = (cartItems, productToAdd) => {
const existingCartItem = cartItems.find(
(cartItem) => cartItem.id === productToAdd.id
);
if (existingCartItem) {
return cartItems.map((cartItem) =>
cartItem.id === productToAdd.id
? { ...cartItem, quantity: cartItem.quantity + 1 }
: cartItem
);
}
return [...cartItems, { ...productToAdd, quantity: 1 }];
};
export const CartContext = createContext({
cartItems: [],
addItemToCart: () => {},
});
export const CartProvider = ({ children }) => {
const [cartItems, setCartItems] = useState([]);
const addItemToCart = (product) => {
setCartItems(addCartItem(cartItems, product));
};
const value = { addItemToCart, cartItems };
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
};
cart.jsx file
import CartItem from "../components/cart-item";
import { toggleClass } from "./navigation";
import { useContext } from "react";
import { CartContext } from "../context/cart.context";
import "../scss/cart.scss";
const Cart = () => {
const { cartItems } = useContext(CartContext);
return (
<div className={`card cart`} style={{ width: "18rem" }}>
<button
type="button"
className="btn-close m-2"
aria-label="Close"
onClick={toggleClass}
></button>
<div className="card-body text-center" style={{ height: "20rem" }}>
<h5 className="card-title">Cart</h5>
<div>
{cartItems.length ? (
cartItems.map((item) => <CartItem key={item.id} cartItem={item} />)
) : (
<h5>The cart is empty</h5>
)}
</div>
<button href="#" className="checkOutBtn mb-2 btn bg-dark text-white">
Check Out
</button>
</div>
</div>
);
};
export default Cart;
cart-item.jsx file
const CartItem = ({ item }) => {
const {name, imageUrl, price, quantity } = item;
return (
<div className="card mb-3">
<div className="row g-0">
<div>
<div className="col-md-4">
<img
src={imageUrl}
className="img-fluid rounded-start"
alt={name}
/>
</div>
<div className="col-md-8">
<div className="card-body">
<h5 className="card-title">{name}</h5>
<p className="card-text">{price}</p>
<p className="card-text">{quantity}</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default CartItem;
productCart.jsx file
import { useContext } from "react";
import { CartContext } from "../context/cart.context";
const ProductCard = ({ product }) => {
const { addItemToCart } = useContext(CartContext);
const addProductToCart = () => addItemToCart(product);
const { name, imageUrl, price } = product;
return (
<div className="col-md-3 my-2">
<div className="card pb-3">
<div
className="image-container"
style={{ height: "300px", overflow: "hidden" }}
>
<img src={imageUrl} className="card-img-top" alt={`${name}`} />
</div>
<div className="card-body d-flex justify-content-between">
<p className="card-text d-inline">{name}</p>
<span className="d-inline text-success">{price}$</span>
</div>
<button
className="btn btn-success w-50 mx-auto"
onClick={addProductToCart}
>
Add to cart
<i className="fa fa-cart-plus mx-2" aria-hidden="true"></i>
</button>
</div>
</div>
);
};
I tried to call the addCartItem function from ProductCard and it worked and added the item to the array but the array didn't update so the cart component didn't render anything cause the array was empty.
NVM guys I found the problem I didn't set up the provider properly in the index.js
What should I've written
<BrowserRouter>
<UserProvider>
<ProductsProvider>
<CartProvider>
<App />
</CartProvider>
</ProductsProvider>
</UserProvider>
</BrowserRouter>
What I wrote
<BrowserRouter>
<UserProvider>
<App />
</UserProvider>
</BrowserRouter>
</React.StrictMode>
i have a reusable contact form that works perfectly when used in the index.js file.
However when i use it from a component in the page folder i am having a 404 not found error message because it uses this route 3000/ourServices/conciergerie/api/contact/ instead of 3000/api/contact.
How do i ensure the it will always fetch the correct route? please see how i fetch the api below :
async function handleSubmit() {
const data = {
firstName,
email,
phone,
message,
};
const res = await fetch("api/contact", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
data: data,
token: "test",
}),
});
alert("Message sent! Thank you\nWe will be in touch with you soon!");
}
pages/ourServices/conciergerie
import Image from "next/image";
import { AiOutlinePlus, AiOutlineMinus } from "react-icons/ai";
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { Contact } from "../../components/contact/Contact";
import es from "../../locales/es-ES/conciergerie.json";
import en from "../../locales/en-US/conciergerie.json";
import Icon1 from "/public/1.svg";
import Icon2 from "/public/2.svg";
import Icon3 from "/public/3.svg";
const Conciergerie = () => {
let { locale } = useRouter();
let t = locale === "es-ES" ? es : en;
// const { t } = useTranslation(locale, "conciergerie");
let myIcons = [Icon1, Icon2, Icon3];
const scrollToConciergerie = () => {
window.scrollTo({
top: 300,
behavior: "smooth",
});
};
const myLoader = ({ src, width, quality }) => {
return `${src}?w=${width}&q=${quality || 75}`;
};
const [showform, setshowform] = useState(false);
useEffect(() => {
window.addEventListener("load", scrollToConciergerie);
return () => {
window.removeEventListener("load", scrollToConciergerie);
};
});
const showContactForm = () => {
return <Contact />;
};
const contentData = t.conciergerieData;
return (
<div className="section" onLoad={scrollToConciergerie}>
<div className="container">
<div className="text-center">
<h1 className=" my-4 text-capitalize" id="conciergerie">
{t.conciergerieHeader}
</h1>
</div>
<h3 className="text-capitalize concierge-subheading mt-3">
{t.conciergerieTitle}
</h3>
<p className="lead concierge-subheading-text">{t.conciergerieText}</p>
</div>
<div className="container">
<div className="row text-center mt-5">
{contentData?.map((item, index) => {
return (
<div className="col-md-4" key={index}>
<span className="fa-stack fa-4x">
<Image
layout="responsive"
src={myIcons[index]}
alt="icons"
className="svg-inline--fa fa-solid fa-stack-1x fa-inverse img-fluid"
aria-hidden="true"
focusable="false"
data-prefix="fas"
data-icon="house"
role="img"
objectFit="cover"
height={300}
width={300}
//loader={myLoader}
/>
</span>
<h4 className="my-3 text-hogar2 text-uppercase">
{item.title}
</h4>
<ul>
{item.text.map((text) => {
return (
<li key={text.id} className="list-unstyled">
<p className="m-0 text-muted text-list">
{text.content}
</p>
</li>
);
})}
</ul>
{item.id === "algomas" &&
(!showform ? (
<AiOutlinePlus
role="button"
onClick={() => {
setshowform(!showform);
}}
className="fs-2"
fill="#5ab4ab"
/>
) : (
<AiOutlineMinus
role="button"
onClick={() => {
setshowform(!showform);
}}
className="fs-2"
fill="#5ab4ab"
/>
))}
{item.id === "else" &&
(!showform ? (
<AiOutlinePlus
role="button"
onClick={() => {
setshowform(!showform);
}}
className="fs-2"
fill="#5ab4ab"
/>
) : (
<AiOutlineMinus
role="button"
onClick={() => {
setshowform(!showform);
}}
className="fs-2"
fill="#5ab4ab"
/>
))}
</div>
);
})}
</div>
{showform && showContactForm()}
</div>
</div>
);
};
export default Conciergerie;
can someone help me please?
The reason this problem is happening has to do with absolute and relative paths.
fetch("api/contact")
Is a relative path. The fetch function figures out the path of the current file, ie 3000/ourServices/conciergerie, and adds api/contact to it
On the other hand, if you add a "/" before the path :
fetch("/api/contact")
Fetch figures out the root path of the project, then adds the path you added, ie :
3000/api/contact
TL;DR: Change fetch("api/contact") to fetch("/api/contact").
I am working on an ecommerce, where I am using material UI pagination component for implementing pagination. Here is new requirement arises. I need to add functionality in pagination: if user click on let's say respectively 3,7,11,13 if they click on browser back button they will go back to 11 then 7 then 3 and lastly 1. How do I do that?
I am using react, react router dom.
Here is pagination structure:
FYI, this is url and API structure:
URL and API structure
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import ProductList from "../../components/common/ProductList/ProductList";
import {
GET_PRODUCTS_BY_BRAND,
GET_PRODUCTS_BY_CATEGORY,
GET_PRODUCTS_BY_SUBCATEGORY,
GET_PRODUCTS_BY_VENDOR,
} from "../../requests/HomePageApi";
import { Pagination } from "#material-ui/lab";
import "./ShopPage.scss";
const ShopPage = () => {
const { type, slug, subcategory } = useParams();
const [loading, setLoading] = useState(true);
// const [error, setError] = useState(false);
const [brands, setBrands] = useState([]);
const [colors, setColors] = useState([]);
const [sizes, setSizes] = useState([]);
const [products, setProducts] = useState(null);
const [filteredProducts, setFilteredProducts] = useState(null);
const [page, setPage] = React.useState(0);
const [count, setCount] = React.useState(1);
const [limit, setLimit] = React.useState(60);
const [total, setTotal] = React.useState(60);
const [sideFilter, setSideFilter] = useState(false);
const [vandor, setvandor] = useState({
vendorImg: "",
vendorName: "",
vendorSlug: "",
});
const [filter, setFilter] = useState({
// brands: "",
color: "",
size: "",
price: "",
});
const closeSideFilter = () => {
setSideFilter(false);
};
const getProducts = async (slug, qParams) => {
try {
let res;
if (type === "category") {
subcategory
? (res = await GET_PRODUCTS_BY_SUBCATEGORY(
slug,
subcategory,
qParams
))
: (res = await GET_PRODUCTS_BY_CATEGORY(slug, qParams));
}
if (type === "brand") res = await GET_PRODUCTS_BY_BRAND(slug, qParams);
if (type === "store") res = await GET_PRODUCTS_BY_VENDOR(slug, qParams);
if (res) setLoading(false);
if (res && res.products && res.products.length > 0) {
setProducts(res.products);
setFilteredProducts(res.products);
setTotal(res.total);
setCount(Math.ceil(res.total / limit));
if (type === "brand") {
setvandor({
vendorImg: `/assets/images/brand/${res.products[0].brand_logo}`,
vendorName: res.products[0].brand_name,
vendorSlug: res.products[0].brand_slug,
});
} else if (type === "store") {
setvandor({
vendorImg: `/assets/images/brand/${res.products[0].brand_logo}`,
vendorName: res.products[0].shop_name,
vendorSlug: res.products[0].vendorSlug,
});
}
if (res.colors) {
const uniqueColors = [...new Set(res.colors)];
setColors(uniqueColors);
}
if (res.sizes) {
const uniqueSizes = [...new Set(res.sizes)];
setSizes(uniqueSizes);
}
// if (res.brands) setBrands(res.brands);
}
} catch (error) {
console.log(error);
}
};
// console.log({ filteredProducts, filter, page, count, limit, total });
React.useMemo(() => {
let qParams = {
page: page,
limit: limit,
size: filter.size,
color: filter.color,
// brands: filter.brands,
price: filter.price.length ? `${filter.price[0]},${filter.price[1]}` : "",
};
if (slug) {
getProducts(slug, qParams);
}
}, [slug, page, limit, filter, count]);
React.useEffect(() => {
setPage(0);
}, [filter]);
const changeLimit = (limit) => {
setPage(0);
setLimit(limit);
};
const handleChange = (event, value) => {
// window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
setPage(value - 1);
};
const slugTitle = (slug) => slug.split("-").join(" ");
return (
<FadeTransition>
{/* {loading && (
<div className="section-big-py-space ratio_asos py-5">
<div className="custom-container">
<Skeleton type="ShopPage" />
</div>
</div>
)} */}
{!loading && products === null && (
<div className="section-big-py-space ratio_asos py-5">
<div className="custom-container">
<h3 style={{ color: "#32375A", textAlign: "center" }}>
Sorry, No Product Found!
</h3>
</div>
</div>
)}
{products && (
<div className="title-slug-section">
<h2 class="title-slug">{slug && slugTitle(slug)}</h2>
</div>
)}
{products && (
<section className="section-big-py-space ratio_asos">
{/* {type !== "category" && (
<div className="merchant-page-header">
<div className="custom-container">
<div
className="shadow-sm bg-white rounded p-3 mb-5 d-flex align-items-center w-100"
style={{ minHeight: "132px" }}
>
<div className="row align-items-center w-100">
<div className="col-lg-6">
<div className="row align-items-center">
{vandor && vandor.vendorImg && (
<div className="col-auto">
<Image
src={vandor.vendorImg}
alt={vandor.vendorName}
className="img-fluid merchant-img"
/>
</div>
)}
<div className="col-auto mt-lg-0 mt-2">
<h3 className="mb-0"> {vandor.vendorName} </h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)} */}
<div className="collection-wrapper">
<div className="custom-container">
<div className="row">
<div className="col-sm-3 collection-filter category-page-side">
{/* <SidebarFilter
type={type}
brands={brands}
colors={colors}
sizes={sizes}
onChange={(data) => setFilter(data)}
/> */}
<InnerCategory />
{products && (
<RowSlider title="New Products" products={products} />
)}
</div>
<div className="collection-content col-lg-9">
<div className="page-main-content">
<div className="collection-product-wrapper">
<div className="row">
<div className="col-xl-12">
{/* <Button
variant='contained'
className='bg-dark text-light d-lg-none mb-3 mt-2 w-100'
onClick={() => setSideFilter(true)}
>
<span className='filter-btn '>
<i
className='fa fa-filter'
aria-hidden='true'
></i>
Filter
</span>
</Button> */}
</div>
</div>
<MainFilter
type={type}
// brands={brands}
colors={colors}
sizes={sizes}
page={page}
limit={limit}
onCountChange={(c) => changeLimit(c)}
onChange={(data) => setFilter(data)}
/>
{/* <TopFilter
onCountChange={(x) => changeLimit(x)}
total={total}
page={page}
limit={limit}
setSideFilter={setSideFilter}
/> */}
{filteredProducts && (
<ProductList products={filteredProducts} />
)}
{count > 1 && (
<div className="d-flex justify-content-center mt-4">
<Pagination
count={count}
page={page + 1}
onChange={handleChange}
shape="rounded"
/>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
)}
{!loading && products?.length === 0 && (
<div className="merchant-page-header">
<div className="custom-container pt-5">
<div
className="shadow-sm bg-white rounded p-3 mb-5 d-flex align-items-center justify-content-center w-100"
style={{ minHeight: "132px" }}
>
<h3 className="mb-0">No Products found!</h3>
</div>
</div>
</div>
)}
<Drawer
open={sideFilter}
className="add-to-cart"
onClose={() => setSideFilter(false)}
transitionDuration={400}
style={{ paddingLeft: "15px" }}
>
<SidebarFilter
onClose={closeSideFilter}
type={type}
// brands={brands}
colors={colors}
sizes={sizes}
onChange={(data) => setFilter(data)}
/>
</Drawer>
</FadeTransition>
);
};
export default ShopPage;
Usually you would like to have URL to represent selected page, so you could refresh the page and still be on the same page. Or share the exact page via copy-paste of URL. Especially on e-commerce sites. So I would recommend to sync selected page with URL.
While it's so common scenario, i have couple hooks for that. First of all - URL hook, which should work with your reactjs app setup.
https://www.npmjs.com/package/hook-use-url
and then couple more hooks to not worry about pagination details inside component:
usePage.js:
import useUrl from "hook-use-url";
export default function usePage() {
const url = useUrl();
const page = url.get({ variable: "page" })
? parseInt(url.get({ variable: "page" }), 10)
: 1;
const setPage = (value) => {
url.multipleActions({
setPairs: [{ variable: "page", value: value }],
});
};
return [page, setPage];
}
and usePerPage.js:
import useUrl from "hook-use-url";
export default function usePerPage() {
const url = useUrl();
const perPage = url.get({ variable: "per-page" })
? parseInt(url.get({ variable: "per-page" }), 10)
: 25;
const setPerPage = (value) => {
url.multipleActions({
setPairs: [
{ variable: "page", value: 1 },
{ variable: "per-page", value },
],
});
};
return [perPage, setPerPage];
}
Inside components you can use these like so:
(Take a note that which page is 1st depends on your backend API, in my case 1st page is always 1 and not 0, but mui.com component starts from 0 that's why there is -1 and +1).
function MyComp(){
const [page, setPage] = usePage();
const [perPage, setPerPage] = usePerPage();
// ....
return (
<TablePagination
count={totalRows}
page={page - 1}
onPageChange={(e, newPage) => {
setPage(newPage + 1);
}}
rowsPerPage={perPage}
onRowsPerPageChange={(e) => {
setPerPage(e.target.value);
}}
/>
)
}
I don't understand why these formatter functions are placed outside the main component called TemplateTable.
If you need to use the actionformatter for example, you cannot pass a dispatch function do it correct?
I had an issue trying to set the actionformatter onClick for the delete button to a dispatch(deleteTemplate()).
When I run the code while the actionformatter is outside the main component TemplateTable, I get dispatch is undefined. When I defined dispatch within the component I obviously get the cannot use react hooks ouside function component problem.
I can fix this whole issue by just including the actionformatter inside the block of templateTable. I just feel like im shortcutting and was wondering if anyone had any input on this
import React, { createRef, Fragment, useState, useEffect} from 'react';
import {
Button,
Card,
CardBody,
Col,
CustomInput,
DropdownItem,
DropdownMenu,
DropdownToggle,
InputGroup,
Media,
Modal,
ModalBody,
Row,
UncontrolledDropdown
} from 'reactstrap';
import { connect, useDispatch } from 'react-redux';
import FalconCardHeader from '../common/FalconCardHeader';
import ButtonIcon from '../common/ButtonIcon';
import paginationFactory, { PaginationProvider } from 'react-bootstrap-table2-paginator';
import BootstrapTable from 'react-bootstrap-table-next';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import { Link } from 'react-router-dom';
import Flex from '../common/Flex';
import Avatar from '../common/Avatar';
import { getPaginationArray } from '../../helpers/utils';
import CreateTemplate from '../templates/CreateTemplate';
import customers from '../../data/e-commerce/customers';
import { listTemplates, deleteTemplate } from '../../actions/index';
const nameFormatter = (dataField, { template }) => {
return (
<Link to="/pages/customer-details">
<Media tag={Flex} align="center">
<Media body className="ml-2">
<h5 className="mb-0 fs--1">{template}</h5>
</Media>
</Media>
</Link>
);
};
const bodyFormatter = (dataField, { avatar, body }) => {
return (
<Link to="/pages/customer-details">
<Media tag={Flex} align="center">
<Media body className="ml-2">
<h5 className="mb-0 fs--1">{body}</h5>
</Media>
</Media>
</Link>
);
};
const emailFormatter = email => <a href={`mailto:${email}`}>{email}</a>;
const phoneFormatter = phone => <a href={`tel:${phone}`}>{phone}</a>;
const actionFormatter = (dataField, { _id }) => (
// Control your row with this id
<UncontrolledDropdown>
<DropdownToggle color="link" size="sm" className="text-600 btn-reveal mr-3">
<FontAwesomeIcon icon="ellipsis-h" className="fs--1" />
</DropdownToggle>
<DropdownMenu right className="border py-2">
<DropdownItem onClick={() => console.log('Edit: ', _id)}>Edit</DropdownItem>
<DropdownItem onClick={} className="text-danger">
Delete
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
);
const columns = [
{
dataField: 'name',
text: 'Name',
headerClasses: 'border-0',
classes: 'border-0 py-2 align-middle',
formatter: nameFormatter,
sort: true
},
{
dataField: 'content',
headerClasses: 'border-0',
text: 'Content',
classes: 'border-0 py-2 align-middle',
formatter: bodyFormatter,
sort: true
},
{
dataField: 'joined',
headerClasses: 'border-0',
text: 'Last modified',
classes: 'border-0 py-2 align-middle',
sort: true,
align: 'right',
headerAlign: 'right'
},
{
dataField: '',
headerClasses: 'border-0',
text: 'Actions',
classes: 'border-0 py-2 align-middle',
formatter: actionFormatter,
align: 'right'
}
];
const SelectRowInput = ({ indeterminate, rowIndex, ...rest }) => (
<div className="custom-control custom-checkbox">
<input
className="custom-control-input"
{...rest}
onChange={() => {}}
ref={input => {
if (input) input.indeterminate = indeterminate;
}}
/>
<label className="custom-control-label" />
</div>
);
const selectRow = onSelect => ({
mode: 'checkbox',
columnClasses: 'py-2 align-middle',
clickToSelect: false,
selectionHeaderRenderer: ({ mode, ...rest }) => <SelectRowInput type="checkbox" {...rest} />,
selectionRenderer: ({ mode, ...rest }) => <SelectRowInput type={mode} {...rest} />,
headerColumnStyle: { border: 0, verticalAlign: 'middle' },
selectColumnStyle: { border: 0, verticalAlign: 'middle' },
onSelect: onSelect,
onSelectAll: onSelect
});
const TemplateTable = ( props ) => {
let table = createRef();
// State
const [isSelected, setIsSelected] = useState(false);
const [showTemplateModal, setShowTemplateModal] = useState(false);
const handleNextPage = ({ page, onPageChange }) => () => {
onPageChange(page + 1);
};
const dispatch = useDispatch()
const deleteHandler = (_id) => {
dispatch(deleteHandler())
}
useEffect(() => {
dispatch(listTemplates())
}, [])
const handlePrevPage = ({ page, onPageChange }) => () => {
onPageChange(page - 1);
};
const onSelect = () => {
setImmediate(() => {
setIsSelected(!!table.current.selectionContext.selected.length);
});
};
const options = {
custom: true,
sizePerPage: 12,
totalSize: props.templates.length
};
return (
<Card className="mb-3">
<FalconCardHeader light={false}>
{isSelected ? (
<InputGroup size="sm" className="input-group input-group-sm">
<CustomInput type="select" id="bulk-select">
<option>Bulk actions</option>
<option value="Delete">Delete</option>
<option value="Archive">Archive</option>
</CustomInput>
<Button color="falcon-default" size="sm" className="ml-2">
Apply
</Button>
</InputGroup>
) : (
<Fragment>
<ButtonIcon onClick={(() => setShowTemplateModal(true))}icon="plus" transform="shrink-3 down-2" color="falcon-default" size="sm">
New Template
</ButtonIcon>
<Modal isOpen={showTemplateModal} centered toggle={() => setShowTemplateModal(!showTemplateModal)}>
<ModalBody className="p-0">
<Card>
<CardBody className="fs--1 font-weight-normal p-4">
<CreateTemplate />
</CardBody>
</Card>
</ModalBody>
</Modal>
<ButtonIcon icon="fa-download" transform="shrink-3 down-2" color="falcon-default" size="sm" className="mx-2">
Download
</ButtonIcon>
<ButtonIcon icon="external-link-alt" transform="shrink-3 down-2" color="falcon-default" size="sm">
Expand View
</ButtonIcon>
</Fragment>
)}
</FalconCardHeader>
<CardBody className="p-0">
<PaginationProvider pagination={paginationFactory(options)}>
{({ paginationProps, paginationTableProps }) => {
const lastIndex = paginationProps.page * paginationProps.sizePerPage;
return (
<Fragment>
<div className="table-responsive">
<BootstrapTable
ref={table}
bootstrap4
keyField="_id"
data={props.templates}
columns={columns}
selectRow={selectRow(onSelect)}
bordered={false}
classes="table-dashboard table-striped table-sm fs--1 border-bottom border-200 mb-0 table-dashboard-th-nowrap"
rowClasses="btn-reveal-trigger border-top border-200"
headerClasses="bg-200 text-900 border-y border-200"
{...paginationTableProps}
/>
</div>
<Row noGutters className="px-1 py-3 flex-center">
<Col xs="auto">
<Button
color="falcon-default"
size="sm"
onClick={handlePrevPage(paginationProps)}
disabled={paginationProps.page === 1}
>
<FontAwesomeIcon icon="chevron-left" />
</Button>
{getPaginationArray(paginationProps.totalSize, paginationProps.sizePerPage).map(pageNo => (
<Button
color={paginationProps.page === pageNo ? 'falcon-primary' : 'falcon-default'}
size="sm"
className="ml-2"
onClick={() => paginationProps.onPageChange(pageNo)}
key={pageNo}
>
{pageNo}
</Button>
))}
<Button
color="falcon-default"
size="sm"
className="ml-2"
onClick={handleNextPage(paginationProps)}
disabled={lastIndex >= paginationProps.totalSize}
>
<FontAwesomeIcon icon="chevron-right" />
</Button>
</Col>
</Row>
</Fragment>
);}
}
</PaginationProvider>
</CardBody>
</Card>
);
};
const mapStateToProps = (state) => {
return {
templates: state.templates,
auth: state.auth,
deleteTemplate: state.deleteTemplate
}
}
export default connect(mapStateToProps, { listTemplates })(TemplateTable);
The short answer is that as-is there isn't any dependency on anything from a consuming component, so it makes complete sense to externalize these declarations from the TemplateTable component.
While you could just move code back into the component to close over any dependencies in the enclosure of the functional component body I think we can do better.
I suggest currying the dispatch function to any of the specific formatters that need it, i.e.
const actionFormatter = ({ dispatch }) => (dataField, { _id }) => (
// Control your row with this id
<UncontrolledDropdown>
<DropdownToggle color="link" size="sm" className="text-600 btn-reveal mr-3">
<FontAwesomeIcon icon="ellipsis-h" className="fs--1" />
</DropdownToggle>
<DropdownMenu right className="border py-2">
<DropdownItem onClick={() => console.log('Edit: ', _id)}>Edit</DropdownItem>
<DropdownItem
onClick={() => dispatch(deleteTemplate())}
className="text-danger"
>
Delete
</DropdownItem>
</DropdownMenu>
</UncontrolledDropdown>
);
And turn columns into a factory function in order to receive and pass on configurations, i.e.
const columns = ({ dispatch }) => ([ // <-- consume config & destructure
{
dataField: 'name',
text: 'Name',
headerClasses: 'border-0',
classes: 'border-0 py-2 align-middle',
formatter: nameFormatter,
sort: true
},
{
dataField: 'content',
headerClasses: 'border-0',
text: 'Content',
classes: 'border-0 py-2 align-middle',
formatter: bodyFormatter,
sort: true
},
{
dataField: 'joined',
headerClasses: 'border-0',
text: 'Last modified',
classes: 'border-0 py-2 align-middle',
sort: true,
align: 'right',
headerAlign: 'right'
},
{
dataField: '',
headerClasses: 'border-0',
text: 'Actions',
classes: 'border-0 py-2 align-middle',
formatter: actionFormatter({ dispatch }), // <-- pass dispatch
align: 'right'
}
]);
Create the columns configuration object and pass to the table.
const config = { dispatch };
...
<BootstrapTable
ref={table}
bootstrap4
keyField="_id"
data={props.templates}
columns={columns(config)} // <-- pass config to factory
selectRow={selectRow(onSelect)}
bordered={false}
classes="table-dashboard table-striped table-sm fs--1 border-bottom border-200 mb-0 table-dashboard-th-nowrap"
rowClasses="btn-reveal-trigger border-top border-200"
headerClasses="bg-200 text-900 border-y border-200"
{...paginationTableProps}
/>