Why arent these functions within the main functional component? - reactjs

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}
/>

Related

testing useQuery inside component can't get the data from msw

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

Currently How to test a callback function with JEST + React?

I would like to test this component:
the parameter received in onClick={() => onConfirmVote(number)}
The value returned in getVoteText
MY CODE:
function CandidateList({ data }: IProps) {
const { convertToCurrency, convertToPercentage } = useNumberConversion();
const getVoteText = useCallback(
(total: number, percentage: number) => `${convertToCurrency(total)} (${convertToPercentage(percentage)})`,
[convertToCurrency, convertToPercentage]
);
return (
<ListGroup data-testid='candidate-list-component'>
{data.map(({ onConfirmVote, avatar, number, votesConfirmed, hasVoted }) => (
<ListGroup.Item key={`${number}`} as='li' className='d-flex flex-wrap gap-2 align-items-center'>
<div className='d-flex justify-content-between align-items-center w-100'>
<div className='d-flex flex-wrap align-items-center gap-2'>
<Avatar alt={`Candidate #${number}`} src={avatar} />
<h5 className='fw-bold mb-0'>#{number}</h5>
</div>
<div className='mt-1 d-flex flex-wrap align-items-center gap-2'>
<span className='mb-1'>Votes:</span>
<Badge bg='primary' pill style={{ width: '155px' }} data-testid='badge-vote-value-component'>
{getVoteText(votesConfirmed.total, votesConfirmed.totalPercentage)}
</Badge>
</div>
</div>
<Button className='w-100' variant='success' onClick={() => onConfirmVote(number)} disabled={hasVoted}>
CONFIRM
</Button>
</ListGroup.Item>
))}
</ListGroup>
);
}
MY TESTE CODE:
describe('[CANDIDATE LIST] - Testing Confirm Vote Button Component', () => {
test('Should be work on click to Confirm Vote!', () => {
render(<CandidateList data={MOCKED_DATA} />);
const confirmVoteButton = screen.getAllByRole('button');
confirmVoteButton.forEach(button => {
console.log(button);
expect(fireEvent.click(button)).toBe(true);
});
});
});
I have simplified your component and you can try sth like this:
the component
import React from "react";
const ManyButtons = ({ data }) => {
const getVoteText = (total, percentage) => `${total} (${percentage})`;
return (
<div>
{data.map(({ onConfirmVote, number, votesConfirmed: { total, totalPercentage } }, index) => (
<div key={index}>
<div>{getVoteText(total, totalPercentage)}</div>
<button onClick={() => onConfirmVote(number)}>
CONFIRM
</button>
</div>
))}
</div>
);
};
export default ManyButtons;
The test:
import { screen, render, fireEvent } from "#testing-library/react";
import ManyButtons from "../ManyButton";
const mockOnConfirmVote = jest.fn();
const mockedData = [
{ onConfirmVote: mockOnConfirmVote, number: 1, votesConfirmed: { total: 1, totalPercentage: 1 } },
{ onConfirmVote: mockOnConfirmVote, number: 2, votesConfirmed: { total: 2, totalPercentage: 2 } },
{ onConfirmVote: mockOnConfirmVote, number: 3, votesConfirmed: { total: 3, totalPercentage: 3 } },
]
describe('it should work', () => {
it('should work', () => {
render(<ManyButtons data={mockedData} />);
const buttons = screen.getAllByRole('button', { name: /confirm/i });
expect(buttons).toHaveLength(mockedData.length);
buttons.forEach(btn => fireEvent.click(btn))
expect(mockOnConfirmVote).toHaveBeenCalledTimes(buttons.length);
mockedData.forEach(({ number, votesConfirmed: { total, totalPercentage }}) => {
expect(mockOnConfirmVote).toHaveBeenCalledWith(number);
expect(screen.getByText(`${total} (${totalPercentage})`)).toBeInTheDocument();
})
})
})

How do I hide a column in a reusable React-Table using initialState but live it present for other pages?

I have this TransactionsSearchResults component where I pass as a child a TransactionsTable. This table is reused in other parts of the application. But when It is rendered in TransactionsSearchResults I need to hide the column balance. I tried using "show" in react table but does not work I tried to use initialState but it was not clear how to use it. Will I have to pass a state prop from TransactionsSearchResults to TransactionsTable and if that prop is true then initialState will be triggered ?
interface Props {
onRowClick?: (
event: MouseEvent<HTMLElement>,
rowData: TransactionData
) => void;
onSuccess: (transaction: TransactionData | undefined) => void;
searchParams: any;
}
const TransactionsSearchResults: FC<Props> = ({
onRowClick,
searchParams,
onSuccess
}) => {
const { currentData, error, isFetching, isSuccess } =
useFetchTransactionsQuery(searchParams);
useEffect(() => {
if (!isFetching && isSuccess) {
onSuccess(currentData?.data[0]);
}
}, [isFetching, isSuccess, currentData]);
const transactions = currentData?.data || ([] as TransactionData[]);
console.log(transactions.length, 'trans');
return (
<div className="search__payouts">
{error && 'error' in error && (
<div className="flex h-screen items-center justify-center">
<div>{error.error}</div>
</div>
)}
{transactions.length > 0 && (
<TransactionsTable
data={transactions}
title={
<>
<span className="font-bold">Search result: </span>
Account Transactions {transactions.length}
</>
}
onRowClick={onRowClick}
/>
)}
{isFetching && (
<div className="flex h-40 w-full items-center justify-center">
Searching transactions
</div>
)}
</div>
);
};
export default TransactionsSearchResults;
This is the table itself. Where I am supposed to use initialState and how I used it. But I am not sure how to pass it only when the parent component is the TransactionsSearchResults. In other words hiding the balance column when this table is rendered in the search result page
interface TransactionsTableProps extends Omit<DataTableProps, 'columns'> {
data: TransactionData[];
onRowClick?: (event: React.MouseEvent<HTMLElement>, rowData: any) => void;
totalCounters?: number;
}
const initialState = { hiddenColumns: ['running_balance'] };
const TransactionsTable: FC<TransactionsTableProps> = (props) => {
const columns = useMemo<ColumnDef<TransactionData>[]>(
() => [
{
id: 'amount_icon',
accessorKey: 'amount',
className: 'w-16 !p-0',
cell: (info) => {
const value = info.getValue<number>();
return (
<div className="relative m-auto w-6">
<DatabaseIcon className="w-6 stroke-inpay-black-haze-500 stroke-1 group-hover:stroke-inpay-cascade" />
{value < 0 && (
<MinusCircleIcon className="absolute top-2.5 left-2.5 w-5 fill-rose-200 stroke-1 " />
)}
{value >= 0 && (
<PlusCircleIcon className="absolute top-2.5 left-2.5 w-5 fill-green-100 stroke-1" />
)}
</div>
);
}
},
// Add spacing (probably not required with a bit of CSS)
{
id: 'space-0',
className: 'w-10'
},
{
id: 'date',
accessorKey: 'created_at',
className: 'grow-0 cursor-pointer',
header: 'Date',
cell: (info) => {
const day = dayjs(info.getValue());
return (
<div className="w-20">
<FormattedDate value={info.getValue()} />
<div className="text-base font-semibold">
{day.format('HH:mm:ss')}
</div>
</div>
);
}
},
{
id: 'id_reference',
header: 'ID/Reference',
accessorKey: 'reference',
className: 'cursor-pointer',
cell: (info) => {
return (
<div className="m-auto text-left uppercase">
<FormattedIdentifier value={info.getValue()} />
</div>
);
}
},
{
id: 'description',
accessorKey: 'entry_type',
header: 'Description',
className: 'cursor-pointer',
cell: (info) => {
return (
<div className="m-auto text-left text-base font-semibold uppercase">
{info.getValue()}
</div>
);
}
},
{
id: 'amount',
accessorFn: (row) => ({
amount: row.amount,
currency: row.currency
}),
className: 'text-right cursor-pointer',
header: 'Amount',
cell: (info) => {
const { currency, amount } = info.getValue<{
amount: number;
currency: string;
}>();
return (
<div className="m-auto ">
<div className="text-xs font-normal uppercase">{currency}</div>
<div className="text-base font-semibold">{amount}</div>
</div>
);
}
},
{
id: 'running_balance',
accessorFn: (row) => ({
running_balance: row.running_balance,
currency: row.currency
}),
initialState,
className: 'text-right cursor-pointer',
enableSorting: false,
header: 'Balance',
cell: (info) => {
const { currency, running_balance } = info.getValue<{
running_balance: number;
currency: string;
}>();
return (
<div className="m-auto">
<div className="text-xs font-normal uppercase">{currency}</div>
<div className="text-base font-semibold">{running_balance}</div>
</div>
);
}
},
// Add spacing (probably not required with a bit of CSS)
{
id: 'space-4',
className: 'min-w-[1rem] w-10'
},
{
id: 'column_params',
className: 'w-10',
cell: () => (
<ChevronRightIcon className="-ml-2 h-7 stroke-inpay-black-haze-700 group-hover:stroke-inpay-black-500 group-active:stroke-inpay-black-500" />
)
}
],
[]
);
return <DataTable columns={columns} {...props} className="min-w-[700px]" />;
};
export default TransactionsTable;

Global Filter in react-table v8 isn't working

I have a slightly modified implementation of the react-table v8 filters: https://tanstack.com/table/v8/docs/examples/react/filters
In the original they seem to use a custom filter function to filter the table globally but it requires another library that i don't want to include, the documentation isn't very clear about this but there seems to be built-in functions that i can use: https://tanstack.com/table/v8/docs/api/features/filters
However the table stops filtering as soon as i change it, i tried not including the globalFilterFn as well as setting it to globalFilterFn: "includesString" which is one of the built-in functions i mentioned but nothing has worked so far.
here is my code:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import {
useReactTable,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
FilterFn,
ColumnDef,
flexRender
} from "#tanstack/react-table";
//import { RankingInfo, rankItem } from "#tanstack/match-sorter-utils";
import { makeData, Person } from "./makeData";
/* declare module "#tanstack/table-core" {
interface FilterMeta {
itemRank: RankingInfo;
}
}
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
// Rank the item
const itemRank = rankItem(row.getValue(columnId), value);
// Store the itemRank info
addMeta({
itemRank
});
// Return if the item should be filtered in/out
return itemRank.passed;
}; */
function App() {
const rerender = React.useReducer(() => ({}), {})[1];
const [globalFilter, setGlobalFilter] = React.useState("");
const columns = React.useMemo<ColumnDef<Person>[]>(
() => [
{
header: "Name",
footer: (props) => props.column.id,
columns: [
{
accessorKey: "firstName",
cell: (info) => info.getValue(),
footer: (props) => props.column.id
},
{
accessorFn: (row) => row.lastName,
id: "lastName",
cell: (info) => info.getValue(),
header: () => <span>Last Name</span>,
footer: (props) => props.column.id
},
{
accessorFn: (row) => `${row.firstName} ${row.lastName}`,
id: "fullName",
header: "Full Name",
cell: (info) => info.getValue(),
footer: (props) => props.column.id
}
]
},
{
header: "Info",
footer: (props) => props.column.id,
columns: [
{
accessorKey: "age",
header: () => "Age",
footer: (props) => props.column.id
},
{
header: "More Info",
columns: [
{
accessorKey: "visits",
header: () => <span>Visits</span>,
footer: (props) => props.column.id
},
{
accessorKey: "status",
header: "Status",
footer: (props) => props.column.id
},
{
accessorKey: "progress",
header: "Profile Progress",
footer: (props) => props.column.id
}
]
}
]
}
],
[]
);
const [data, setData] = React.useState(() => makeData(500));
const refreshData = () => setData((old) => makeData(500));
const table = useReactTable({
data,
columns,
state: {
globalFilter
},
onGlobalFilterChange: setGlobalFilter,
//globalFilterFn: "includesString",
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel()
});
return (
<div className="p-2">
<div>
<input
value={globalFilter ?? ""}
onChange={(event) => setGlobalFilter(event.target.value)}
className="p-2 font-lg shadow border border-block"
placeholder="Search all columns..."
/>
</div>
<div className="h-2" />
<table>
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th key={header.id} colSpan={header.colSpan}>
{header.isPlaceholder ? null : (
<>
<div
{...{
className: header.column.getCanSort()
? "cursor-pointer select-none"
: "",
onClick: header.column.getToggleSortingHandler()
}}
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
{{
asc: " 🔼",
desc: " 🔽"
}[header.column.getIsSorted() as string] ?? null}
</div>
</>
)}
</th>
);
})}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
return (
<tr key={row.id}>
{row.getVisibleCells().map((cell) => {
return (
<td key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
<div className="h-2" />
<div className="flex items-center gap-2">
<button
className="border rounded p-1"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
{"<<"}
</button>
<button
className="border rounded p-1"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{"<"}
</button>
<button
className="border rounded p-1"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{">"}
</button>
<button
className="border rounded p-1"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
{">>"}
</button>
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{" "}
{table.getPageCount()}
</strong>
</span>
<span className="flex items-center gap-1">
| Go to page:
<input
type="number"
defaultValue={table.getState().pagination.pageIndex + 1}
onChange={(e) => {
const page = e.target.value ? Number(e.target.value) - 1 : 0;
table.setPageIndex(page);
}}
className="border p-1 rounded w-16"
/>
</span>
<select
value={table.getState().pagination.pageSize}
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[10, 20, 30, 40, 50].map((pageSize) => (
<option key={pageSize} value={pageSize}>
Show {pageSize}
</option>
))}
</select>
</div>
<div>{table.getPrePaginationRowModel().rows.length} Rows</div>
<div>
<button onClick={() => rerender()}>Force Rerender</button>
</div>
<div>
<button onClick={() => refreshData()}>Refresh Data</button>
</div>
<pre>{JSON.stringify(table.getState(), null, 2)}</pre>
</div>
);
}
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Failed to find the root element");
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
a link to CodeSanbox with it: https://codesandbox.io/s/long-monad-652jcm?file=/src/main.tsx
I'm still very inexperienced in React and even more with typescript so maybe i'm missing something obvious.
am i misinterpreting the docs, maybe the custom function is necessary?
There is a current issue on GitHub where you can't use a global filter if one of your column is using number fields. You can simply solve it by replacing the number by a string in your data. (.toString(), and sorting still works). If worked fine for me afterwards. :)
Your globalFilter doesn't do anything, you need to test if the contents of the table are equal to the filter or not.

Using React-bootstrap, why is the first accordion not opening on load page or toggling

I am trying to toggle the accordion which works perfectly but unfortunately the first which is default isn't opening the body by default. I don't know why it would behave differently as I have all the code in loop, and other accordion bodies are displaying and working perfectly.
import React, {useState} from 'react'
import SiteLayout from '../Components/SiteLayout/SiteLayout'
import {Accordion, Card , useAccordionToggle }from 'react-bootstrap'
import {
faPlus,
faMinus
} from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
function CustomToggle({ children, eventKey, handleClick }) {
const decoratedOnClick = useAccordionToggle(eventKey, () =>
handleClick()
);
return (
<button
type="button"
style={{ backgroundColor: 'transparent' }}
onClick={decoratedOnClick}
>
{children}
</button>
);
}
export default function Resources() {
const [activeKey, setActiveKey] = useState(0)
const data = [
{
title: 'Port-Harcourt',
content: 'Rough but very place '
},
{
title: 'Abuja',
content: 'Neat but a lot of diplomacy '
},
{
title: 'Lagos',
content: 'Neat and Rough, but a lot of politics'
},
]
return (
<SiteLayout>
<Accordion defaultActiveKey={0} activeKey={activeKey}>
{
data.map((item, index) => (
<Card key={index}>
<Card.Header> {item.title}
<CustomToggle eventKey={index}
handleClick ={() => {
if (activeKey === index ) {
setActiveKey(null)
}else {
setActiveKey(index)
}
}}
>
{activeKey === index ? <FontAwesomeIcon icon={faMinus} /> : <FontAwesomeIcon icon={faPlus} />}
</CustomToggle>
</Card.Header>
<Accordion.Collapse eventKey={index}>
<Card.Body>
{item.content}
</Card.Body>
</Accordion.Collapse>
</Card>
))
}
</Accordion>
</SiteLayout>
)
}
I don't know what I am doing wrong and why the first accordion body isn't displaying. Thank you
EventKey is a String type variable, your initial value is a number type variable. You can change the initial value of the active key to a string value and convert the index value to a string in the collapse.
import React, {useState} from 'react'
import {Accordion, Card , useAccordionToggle }from 'react-bootstrap'
import {
faPlus,
faMinus
} from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/react-fontawesome";
function CustomToggle({ children, eventKey, handleClick }) {
const decoratedOnClick = useAccordionToggle(eventKey, () =>
handleClick()
);
return (
<button
type="button"
style={{ backgroundColor: 'transparent' }}
onClick={decoratedOnClick}
>
{children}
</button>
);
}
export default function Resources() {
const [activeKey, setActiveKey] = useState("0")
const data = [
{
title: 'Port-Harcourt',
content: 'Rough but very place '
},
{
title: 'Abuja',
content: 'Neat but a lot of diplomacy '
},
{
title: 'Lagos',
content: 'Neat and Rough, but a lot of politics'
},
]
return (
<Accordion defaultActiveKey={"0"} activeKey={activeKey}>
{
data.map((item, index) => (
<Card key={index}>
<Card.Header> {item.title}
<CustomToggle eventKey={index}
handleClick ={() => {
if (activeKey === index ) {
setActiveKey(null)
}else {
setActiveKey(index)
}
}}
>
{activeKey === index ? <FontAwesomeIcon icon={faMinus} /> : <FontAwesomeIcon icon={faPlus} />}
</CustomToggle>
</Card.Header>
<Accordion.Collapse eventKey={index.toString()}>
<Card.Body>
{item.content}
</Card.Body>
</Accordion.Collapse>
</Card>
))
}
</Accordion>
)
}

Resources