Infinite call renderCell in React - reactjs

I'm using function component to create a MUI dataGrid, and trying to add a button in a column, and I have a onRowClick function to open a side pane when user clicking row. The problem is, once I click row, react will report error:
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Here is the code:
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
};
const formatDates = (columns): GridColDef[] => {
return columns;
};
const addTooltipsToData = (columns: GridColDef[]): GridColDef[] => {
console.log('render tool bar called');
return columns.map((column) => {
const { description, field, headerName } = column;
console.log('inside map');
if (field === ID) {
console.log('直接return');
return column;
}
return {
...column,
renderCell: (): JSX.Element => {
console.log('render run');
return (
<Tooltip arrow title={description || ''} >
<span className={classes.headerCell}>{headerName}</span>
</Tooltip>
);
},
};
});
};
const formatColumns = (columns: GridColDef[]): GridColDef[] => {
const dateFormatted = formatDates(columns);
return addTooltipsToData(dateFormatted);
};
console.log('generic table rendered');
return (
<MuiThemeProvider theme={theme}>
<DataGrid
columns={formatColumns(columns)}
rows={rows}
autoHeight
className={classes.table}
components={{
Toolbar: CustomToolbar,
}}
density={GridDensityTypes.Compact}
filterMode={tableMode}
hideFooterSelectedRowCount
loading={loading}
onFilterModelChange={handleFilterChange}
onSortModelChange={handleSortChange}
sortModel={sortModel}
sortingMode={tableMode}
onRowClick={openViewPane}
/>
</MuiThemeProvider>
);
However, if I change the renderCell to renderHeader, it will work fine.
setRightSlidePlaneContent
setRightSlidePlaneOpen
Above are two state passed by parent component in props. it will open a slide pane.
After I comment setRightSliePlaneOpen, it will work well. But no slide pane show.
Please help me slove it. Or do you know how can I add a button in column not using renderCell?
const PageFrame: FC<IProps> = (props: IProps) => {
const classes = useStyles();
const dispatch = useAppDispatch();
const { Component, userInfo } = props;
const [navBarOpen, setNavBarOpen] = useState(false);
const [rightSlidePlaneOpen, setRightSlidePlaneOpen] = useState(false);
const [rightSlidePlaneContent, setRightSlidePlaneContent] = useState(
<Fragment></Fragment>,
);
const [rightSlidePlaneWidthLarge, setRightSlidePlaneWidthLarge] = useState(
false,
);
useEffect(() => {
dispatch({
type: `${GET_USER_LOGIN_INFO}_${REQUEST}`,
payload: {
empId: userInfo.empId,
auth: { domain: 'GENERAL_USER', actionType: 'GENERAL_USER', action: 'VIEW', empId: userInfo.empId},
},
meta: { remote: true },
});
}, []);
return (
<div className={classes.root}>
<HeaderBar
navBarOpen={navBarOpen}
toggleNavBarOpen={setNavBarOpen}
/>
<NavigationBar open={navBarOpen} toggleOpen={setNavBarOpen} />
<Component
setRightSlidePlaneContent={setRightSlidePlaneContent}
setRightSlidePlaneOpen={setRightSlidePlaneOpen}
setRightSlidePlaneWidthLarge={setRightSlidePlaneWidthLarge}
/>
<PersistentDrawerRight
content={rightSlidePlaneContent}
open={rightSlidePlaneOpen}
rspLarge={rightSlidePlaneWidthLarge}
/>
</div>
);
};
export default PageFrame;
The component that calls setRightSidePlaneOpen
interface IProps {
setRightSlidePlaneContent: React.Dispatch<React.SetStateAction<JSX.Element>>;
setRightSlidePlaneOpen: React.Dispatch<React.SetStateAction<boolean>>;
setRightSlidePlaneWidthLarge: React.Dispatch<SetStateAction<boolean>>;
}
const TagDashboard = (props: IProps): JSX.Element => {
const { setRightSlidePlaneContent, setRightSlidePlaneOpen, setRightSlidePlaneWidthLarge } = props;
const employeeId = useAppSelector((store) => store.userInfo.info.employeeNumber);
const rows = useAppSelector((state) => state.tag.rows);
const accountId = useAppSelector(store => store.userInfo.accountId);
const updateContent = useAppSelector(state => state.tag.updateContent);
const numOfUpdates = useAppSelector(state => state.tag.numOfUpdates);
const dispatch = useAppDispatch();
const closeAddForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
};
const openAddForm = (): void => {
setRightSlidePlaneContent(
<AddForm
category={'tag'}
close={closeAddForm}
title={ADD_FORM_TITLE}
createFunction={createTag}
/>);
setRightSlidePlaneOpen(true);
};
const closeForm = (): void => {
setRightSlidePlaneContent(<Fragment />);
setRightSlidePlaneOpen(false);
setRightSlidePlaneWidthLarge(false);
};
const openViewPane = (params: GridRowParams, e): void => {
setRightSlidePlaneContent(
<ViewAccountPane
close={closeForm}
params={params}
/>,
);
setRightSlidePlaneOpen(true);
setRightSlidePlaneWidthLarge(true);
};
// to the RSP.
return (
<GenericDashboard
addFunction={openAddForm}
description={DESCRIPTION}
title={TITLE}
columns={columns}
handleRowClick={openViewPane}
rows={rows}
numOfUpdates={numOfUpdates}
updateContent={updateContent}
/>
);
};
This is the component of the right slide pane
const { content, open, rspLarge } = props;
const classes = useStyles();
const drawerClass = rspLarge ? classes.drawerLarge : classes.drawer;
const drawerPaperClass = rspLarge ? classes.drawerPaperLarge : classes.drawerPaper;
return (
<div className={classes.root}>
<CssBaseline />
<Drawer
className={drawerClass}
variant='temporary'
anchor='right'
open={open}
classes={{
paper: drawerPaperClass,
}}
>
<Fragment>{content}</Fragment>
</Drawer>
</div>
);

Related

react typescript jotai Property 'placement' does not exist on type 'NotAnArrayType<unknown>'

I'm trying to convert an an app from javascript to typescript and I'm having issues. I'm new to typescript and Jotai. I'm getting the error in the Attribute function on attribute?placement. It's like it doesn't recognize that it is an array of strings. Can someone please help me decipher what I'm doing wrong.
import { splitAtom } from "jotai/utils";
import { focusAtom } from 'jotai-optics';
import { atom, useAtom, useAtomValue } from "jotai";
import cat from '../cat.json';
import { Key, useMemo } from "react";
import { Switch } from "#chakra-ui/react";
const catAtom = atom(cat);
const partsAtom = focusAtom(catAtom, (optic) => optic.prop("parts"));
const partsAtomAtoms = splitAtom(partsAtom);
const useAttributesAtom = (partAtom:any) => {
return useMemo(
() => focusAtom(partAtom, (optic:any) => optic.prop("attributes")),
[partAtom]
);
};
const useAttributeAtom = (attributesAtom:any, index:number) => {
// const { attributesAtom, index } = props;
return useMemo(() => {
return focusAtom(attributesAtom, (optic) => optic.at(index));
}, [attributesAtom, index]);
};
const Attribute = (attributesAtom:any, index:number) => {
// const { attributesAtom, index } = props;
const attributeAtom = useAttributeAtom(attributesAtom, index);
const [attribute, setAttribute] = useAtom(attributeAtom);
return (
<div style={{ display: "flex" }}>
<label>
<span style={{ marginRight: "16px" }}>{attribute?.placement}</span>
<Switch
onChange={(checked) =>
setAttribute((prevAttribute: any) => ({
...prevAttribute,
injured: checked
}))
}
checked={attribute?.injured}
/>
</label>
</div>
);
};
const Part = (partAtom:any) => {
const [part] = useAtom(partAtom) as typeof partAtom;
const attributesAtom = useAttributesAtom(partAtom);
const attributes = useAtomValue(attributesAtom) as typeof partAtom;
return (
<div>
<h3>{part.type}</h3>
{attributes.map((attribute: { placement: Key | null | undefined; }, index:
number) => {
return (
<Attribute
key={attribute.placement}
attributesAtom={attributesAtom}
index={index}
/>
);
})}
</div>
);
};
const PetParts = () => {
const [partsAtoms] = useAtom(partsAtomAtoms);
return (
<div>
<h2>Body Injury Details</h2>
{partsAtoms.map((partAtom) => {
return <Part key={`${partAtom}`} partAtom={partAtom} />;
})}
</div>
);
};
export default PetParts;

Cannot pass selected date from MUI DateTimePicker into SWR hook

I have a component that holds a MUI DataGrid.
Now, for any row of the DataGrid I need to render a DateTimePicker column. There is data coming
in with a SWR call which data could contain a datetime for the column or not.
mainPage/sdk.js
export const useSomeData = (
params: TUseFetchOptions['params']
) => {
const { data, isLoading, mutate } = useFetch<TPagination<TSomeData>>('/some-data/');
const approve = useCallback(
(someData: TSomeData) => {
const data = { published_at: someData.published_at }
return requestHandler(
post(`/some-data/update/`, data)
)
.then((someData) => {
mutate((prev) => {
if (!prev) {
return prev;
}
// some irrelevant mutation for swr caching happens here
return prev;
}
return prev;
}, false);
})
.catch((error) =>
// some irrelevant alerting happens here
);
},
[mutate]
);
return useMemo(
() => ({ someData: data, isLoading, approve,mutate }),
[data, isLoading, mutate, approve]
);
};
mainPage/index.tsx
import {useSomeData} from './sdk'
const SomeDataPublish = () => {
// const {params} = ....
// const dataGridProps = ...
const { someData, isLoading, approve } = useSomeData(params);
return (
<Stack>
{someData && (
<SomeDataDataGrid
someData={someData}
params={params}
DataGridProps={dataGridProps}
handleApprove={approve}
/>
)}
</Stack>
);
};
export default SomeDataPublish;
mainPage/componenets/someDataDataGrid.tsx
export const columns: GridColumns = [
{
// some field
},
{
// some field
},
{
// some field
},
// ...
];
const buildColumnsData = (
handleApprove: ReturnType<typeof useSomeData>['approve'],
): GridColumns => {
return [
...columns,
{
field: 'published_at',
headerName: 'Publish at',
flex: 0.75,
renderCell: (params: any) => <RowDatePicker params={params} />
},
{
field: '',
type: 'actions',
flex: 0.4,
getActions: (params: any) => [
<RowActions
params={params}
handleApprove={handleApprove}
/>
]
}
];
};
const buildRows = (someData: TSomeData[]): GridRowsProp => {
return someData.map((row) => ({
id: row.id,
// ...
published_at: _.get(row, 'published_at'),
}));
};
const SomeDataDataGrid: FC<{
someData: TPagination<TSomeData>;
params: TUseFetchOptions['params'];
DataGridProps: Partial<MuiDataGridProps>;
handleApprove: ReturnType<typeof useSomeData>['approve'];
}> = ({ someData, params, DataGridProps, handleApprove }) => {
return (
<Paper>
<DataGrid
// ...
columns={buildColumnsData(handleApprove)}
rows={someData ? buildRows(someData.results) : []}
// ...
{...DataGridProps}
/>
</Paper>
);
};
export default SomeDataDataGrid;
mainPage/componenets/rowDatePicker.tsx
const RowDatePicker: React.FC<{
params: GridRowParams;
}> = ({ params }) => {
const [publishedAt, setPublishedAt] = React.useState(params.row.published_at);
return (
<>
<DateTimeField
label={'Pick Date'}
value={publishedAt}
onChange={setPublishedAt}
/>
</>
);
};
export default RowDatePicker;
mainPage/componenets/rowAction.tsx
const RowActions: React.FC<{
params: GridRowParams;
handleApprove: ReturnType<typeof useSomeData>['approve'];
}> = ({ params, handleApprove }) => {
return (
<>
<Tooltip title="Approve">
<IconButton
color="success"
disabled={false}
onClick={(e) => {
console.log(params.row)}
handleApprove(params.row)
>
<AppIcons.CheckCircle />
</IconButton>
</Tooltip>
</>
);
};
export default RowActions;
The problem that I have - if I change the date from the date picker, on clicking the <AppIcons.CheckCircle /> in the RowActions component I expect the row.published_at to be updated with the new value. Then I pass the new updated object (with the updated published_at attribute) to the handleApprove hook so I can make some mutations and pass the updated object (with new published_at value) to the back end.
However, on examining the someData object that is passed to the approve hook the published_at field has its old value ( the one that came from the SWR fetcher).
I know that I need to mutate somehow params.row.published_at = publishedAt in the onChange callback of the RowDatePicker.DateTimePicker, but I am not sure how to do it. Any help would be appreciated.

pass coordinates from googlemaps react child to parent in functional component typescript

I'm using #googlemaps/react-wrapper to make a map component in my react application using the example from googlemaps, and adding an event on drag marker to refresh coordinates, this works fine now. but i need to call the map component outside to refresh a input value with the coordiantes.
The error i get it is:
Binding element 'childToParent' implicitly has an 'any' type.*
Please could help me to understand how i could send the values to paren using typescript
Greetings
In parent i have this
const [coordinate,SetCoordinate]=useState("");
return (
<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={3}>
<RHFTextField name="lat" label="Coord X" />
<RHFTextField name="lng" label="Coord Y" />
</Stack>
<Stack>
<br/>
<LocationMap childToParent={setCoordinate}/>
</Stack>
<Stack>
<LoadingButton
fullWidth
size="large"
type="submit"
variant="contained"
>
Save
</LoadingButton>
</Stack>
</FormProvider>
);
My Location map component is like this
const render = (status: Status) => {
return <h1>{status}</h1>;
};
interface MapProps extends google.maps.MapOptions {
style: { [key: string]: string };
onClick?: (e: google.maps.MapMouseEvent) => void;
onIdle?: (map: google.maps.Map) => void;
}
//function to pass value to parent
interface LocationProps {
childToParent: (arg0: string)=>string;
}
export default function LocationMap({childToParent,...props}){
const [clicks, setClicks] = useState<google.maps.LatLng[]>([]);
const [zoom, setZoom] = useState(3); // initial zoom
const [center, setCenter] = useState<google.maps.LatLngLiteral>({
lat: 0.0,
lng: 0.0,
});
const [markerLocation, setMarkerLocation] = useState<google.maps.LatLng>();
const dragend = (e: google.maps.MapMouseEvent) => {
// avoid directly mutating state
setMarkerLocation(e.latLng!)
setClicks([...clicks, e.latLng!]);
};
const onClick = (e: google.maps.MapMouseEvent) => {
};
const onIdle = (m: google.maps.Map) => {
//.log("onIdle");
setZoom(m.getZoom()!);
setCenter(m.getCenter()!.toJSON());
};
const ref = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<google.maps.Map>();
useEffect(() => {
if (ref.current && !map) {
setMap(new window.google.maps.Map(ref.current, {}));
}
}, [ref, map]);
return (
<>
<div style={{ display: "flex", height: "100%" }}>
<Wrapper apiKey={apiKey} render={render}>
<Map
center={center}
onClick={onClick}
onIdle={onIdle}
zoom={zoom}
style={{ flexGrow: "1", height: "25em", width: "400px" }}
>
<Marker key="point" draggable={true} dragend={dragend} />
</Map>
</Wrapper>
</div>
<div id="coordinate">
{clicks.map(function (latLng, i, row) {
var element = document.getElementById("coordenadas");
if (element === null) {
console.error("error cleaning coordinates");
} else {
element.innerHTML = "";
}
return (
childToParent(latLng.toJSON())
);
})
}
</div>
</>
)
};
interface MapProps extends google.maps.MapOptions {
onClick?: (e: google.maps.MapMouseEvent) => void;
onIdle?: (map: google.maps.Map) => void;
}
const Map: React.FC<MapProps> = ({
onClick,
onIdle,
children,
style,
...options
}) => {
const ref = useRef<HTMLDivElement>(null);
const [map, setMap] = useState<google.maps.Map>();
useEffect(() => {
if (ref.current && !map) {
setMap(new window.google.maps.Map(ref.current, {}));
}
}, [ref, map]);
// because React does not do deep comparisons, a custom hook is used
// see discussion in https://github.com/googlemaps/js-samples/issues/946
useDeepCompareEffectForMaps(() => {
if (map) {
map.setOptions(options);
}
}, [map, options]);
useEffect(() => {
if (map) {
["click", "idle"].forEach((eventName) =>
google.maps.event.clearListeners(map, eventName)
);
if (onClick) {
map.addListener("click", onClick);
}
if (onIdle) {
map.addListener("idle", () => onIdle(map));
}
}
}, [map, onClick, onIdle]);
return (
<>
<div ref={ref} style={style} />
{Children.map(children, (child) => {
if (isValidElement(child)) {
// set the map prop on the child component
return cloneElement(child, { map });
}
})}
</>
);
};
interface MarkerProps extends google.maps.MarkerOptions {
dragend?: (e: google.maps.MapMouseEvent) => void;
}
const Marker: React.FC<MarkerProps> = ({
dragend,
...options
}) => {
const [marker, setMarker] = useState<google.maps.Marker>();
console.log(options);
useEffect(() => {
if (!marker) {
setMarker(new google.maps.Marker({
position: {
lat: 0,
lng: 0,
},
}));
}
// remove marker from map on unmount
return () => {
if (marker) {
marker.setMap(null);
}
};
}, [marker]);
useEffect(() => {
if (marker) {
marker.setOptions(options);
}
}, [marker, options]);
useEffect(() => {
if (marker) {
["dragend"].forEach((eventName) =>
google.maps.event.clearListeners(marker, eventName)
);
marker.setOptions(options);
if (dragend) {
//map.addListener("click", onClick);
marker.addListener("dragend", dragend);
}
}
}, [marker, dragend, options]);
return null;
};
const deepCompareEqualsForMaps = createCustomEqual(
(deepEqual) => (a: any, b: any) => {
if (
isLatLngLiteral(a) ||
a instanceof google.maps.LatLng ||
isLatLngLiteral(b) ||
b instanceof google.maps.LatLng
) {
return new google.maps.LatLng(a).equals(new google.maps.LatLng(b));
}
// TODO extend to other types
// use fast-equals for other objects
return deepEqual(a, b);
}
);
function useDeepCompareMemoize(value: any) {
const ref = useRef();
if (!deepCompareEqualsForMaps(value, ref.current)) {
ref.current = value;
}
return ref.current;
}
function useDeepCompareEffectForMaps(
callback: React.EffectCallback,
dependencies: any[]
) {
useEffect(callback, dependencies.map(useDeepCompareMemoize));
}
export default LocationMap;
It is warning you because you have not passed the correct function. This should fix the problem:
const [coordinate, setCoordinate] = useState("");
const deepCompareEqualsForMaps = createCustomEqual((deepEqual: any, ref: any) => (a: any, b: any) => {
if (
isLatLngLiteral(a) ||
a instanceof window.google.maps.LatLng ||
isLatLngLiteral(b) ||
b instanceof window.google.maps.LatLng
) {
return new window.google.maps.LatLng(a).equals(new window.google.maps.LatLng(b));
}
// TODO extend to other types
// use fast-equals for other objects
return deepEqual(a, b);
}
);

How to test onClick() funct and useState hooks using jest and enzyme

I am new to this jest+enzyme testing and I am stuck at how to cover the lines and functions such as onClick(), the useState variables and also useffect(). Can anyone with any experience in such scenerios please give me some direction on how to do that efficiently.
Below is the code:
export interface TMProps {
onClick: (bool) => void;
className?: string;
style?: object;
}
export const TM: React.FC<TMProps> = (props) => {
const {onClick} = props;
const [isMenuOpen, toggleMenu] = useState(false);
const handleUserKeyPress = (event) => {
const e = event;
if (
menuRef &&
!(
(e.target.id && e.target.id.includes("tmp")) ||
(e.target.className &&
(e.target.className.includes("tmp-op") ||
e.target.className.includes("tmp-option-wrapper")))
)
) {
toggleMenu(false);
}
};
useEffect(() => {
window.addEventListener("mousedown", handleUserKeyPress);
return () => {
window.removeEventListener("mousedown", handleUserKeyPress);
};
});
return (
<React.Fragment className="tmp">
<Button
className={props.className}
style={props.style}
id={"lifestyle"}
onClick={() => toggleMenu((state) => !state)}>
Homes International
<FontAwesomeIcon iconClassName="fa-caret-down" />{" "}
</Button>
<Popover
style={{zIndex: 1200}}
id={`template-popover`}
isOpen={isMenuOpen}
target={"template"}
toggle={() => toggleMenu((state) => !state)}
placement="bottom-start"
className={"homes-international"}>
<PopoverButton
className={
"template-option-wrapper homes-international"
}
textProps={{className: "template-option"}}
onClick={() => {
onClick(true);
toggleMenu(false);
}}>
Generic Template{" "}
</PopoverButton>
/>
}
Here is the test I have written but it isn't covering the onClick(), useEffect() and handleUserKeyPress() function.
describe("Modal Heading", () => {
React.useState = jest.fn().mockReturnValueOnce(true)
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Button)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Popover)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(PopoverButton)).toHaveLength(1);
});
What you're looking for is enzyme's:
const btn = wrapper.find('lifestyle');
btn.simulate('click');
wrapper.update();
Not sure if it'd trigger the window listener, it's possible you'll have to mock it.

Getting selected items in Fluent UI DetailsList

I am using Fluent UI DetailsList. In the example the component is implemented as a class component but I am using a functional component.
I am having difficulties in getting the selected items, I assume and think my implementation is incorrect. The problem is I do not get ANY selected items.
export const JobDetails = () => {
const { actions, dispatch, isLoaded, currentTabJobs, activeTabItemKey } = useJobDetailsState()
let history = useHistory();
useEffect(() => {
if (actions && dispatch) {
actions.getJobListDetails()
}
}, [actions, dispatch])
const getSelectionDetails = (): string => {
let selectionCount = selection.getSelectedCount();
switch (selectionCount) {
case 0:
return 'No items selected';
case 1:
return '1 item selected: ' + (selection.getSelection()[0] as any).name;
default:
return `${selectionCount} items selected`;
}
}
const [selectionDetails, setSelectionDetails] = useState({})
const [selection, setSelection] = useState(new Selection({
onSelectionChanged: () => setSelectionDetails(getSelectionDetails())
}))
useEffect(() => {
setSelection(new Selection({
onSelectionChanged: () => setSelectionDetails(getSelectionDetails())
}))
},[selectionDetails])
return (
<div>
<MarqueeSelection selection={selection}>
<DetailsList
items={currentTabJobs}
groups={getGroups()}
columns={_columns}
selection={selection}
selectionPreservedOnEmptyClick={true}
groupProps={{
onRenderHeader: props => {
return (
<GroupHeader
{...props}
selectedItems={selection}
/>
)
},
showEmptyGroups: true
}}
/>
</MarqueeSelection>
</div>
)
}
export default JobDetails;
I might have a more simple answer, this example is for a list with 'SelectionMode.single' activated but I think the principle of getting the selected item remains the same
const [selectedItem, setSelectedItem] = useState<Object | undefined>(undefined)
const selection = new Selection({
onSelectionChanged: () => {
setSelectedItem(selection.getSelection()[0])
}
})
useEffect(() => {
// Do something with the selected item
console.log(selectedItem)
}, [selectedItem])
<DetailsList
columns={columns}
items={items}
selection={selection}
selectionMode={SelectionMode.single}
selectionPreservedOnEmptyClick={true}
setKey="exampleList"
/>
I found a solution to the problem I was having and I had to memorize the details list
What I did:
const [selectedItems, setSelectedItems] = useState<IObjectWithKey[]>();
const selection = useMemo(
() =>
new Selection({
onSelectionChanged: () => {
//console.log('handle selection change',selection.getSelection())
setSelectedItems(selection.getSelection());
},
selectionMode: SelectionMode.multiple,
}),
[]);
const detailsList = useMemo(
() => (
<MarqueeSelection selection={selection}>
<DetailsList
items={currentTabJobs}
groups={getGroups()}
columns={columns}
ariaLabelForSelectAllCheckbox="Toggle selection for all items"
ariaLabelForSelectionColumn="Toggle selection"
checkButtonAriaLabel="Row checkbox"
selection={selection}
selectionPreservedOnEmptyClick={true}
groupProps={{
onRenderHeader: (props) => {
return <GroupHeader {...props} selectedItems={selection} />;
},
showEmptyGroups: true,
}}
onRenderItemColumn={(item, index, column) =>
renderItemColumn(item, index!, column!)
}
/>
</MarqueeSelection>
),
[selection, columns, currentTabJobs, activeTabItemKey]
);
return (
<div>
{detailsList}
</div>
)
Put the selection object in a state.
Example:
...
export const Table: FunctionComponent<TableProps> = props => {
const { items, columns } = props
const { setCopyEnabled } = useCommandCopy()
const { setDeleteEnabled } = useCommandDelete()
const onSelectionChanged = () => {
if (selection.getSelectedCount() === 0) {
setCopyEnabled(false)
setDeleteEnabled(false)
}
else if (selection.getSelectedCount() === 1) {
setCopyEnabled(true)
setDeleteEnabled(true)
}
else {
setCopyEnabled(false)
setDeleteEnabled(true)
}
}
...
const [selection] = useState(new Selection({ onSelectionChanged: onSelectionChanged }))
useEffect(() => {
selection.setAllSelected(false)
}, [selection])
...
return (
<ScrollablePane styles={{
root: {
position: 'fixed',
top: 105, left: 285, right: 20, bottom: 20
},
}}>
<DetailsList
items={items}
columns={columns}
selection={selection}
selectionMode={SelectionMode.multiple}
layoutMode={DetailsListLayoutMode.justified}
constrainMode={ConstrainMode.horizontalConstrained}
...
/>
</ScrollablePane>
)
}
I think the main issue here is onSelectionChanged function is getting called twice, second time with empty data. Reason I found is React useState method re-rendering the data. Solution that worked for me here :
Store value in a normal variable instead of state variable(if you don't want to re-render detailslist after this):
let selectedItem = undefined;
const selection = new Selection({
onSelectionChanged: () => {
selectedItem = selection.getSelection()
// console.log(selectedItem)
// You can use selectedItem value later anywhere you want to
// track your selection.
}
})
<DetailsList
columns={columns}
items={items}
selection={selection}
selectionMode={SelectionMode.multiple}
selectionPreservedOnEmptyClick={true}
setKey="exampleList"
/>

Resources