Use DataList
Summary: With new abstraction, `DataList` matches what the plugin trying to render. Should fix: https://fb.workplace.com/groups/flippersupport/permalink/1145431339270856/ Changelog: [MobileConfig] Fix issues with scrolling not working and several other improvements Reviewed By: cekkaewnumchai Differential Revision: D28314408 fbshipit-source-id: 4d8fbe3d8e868f737750203cd568d94bae8b4108
This commit is contained in:
committed by
Facebook GitHub Bot
parent
34c862d5f2
commit
0aadb862ee
@@ -7,14 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, {memo, createRef, useMemo, useEffect, useCallback} from 'react';
|
||||||
useCallback,
|
|
||||||
memo,
|
|
||||||
createRef,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import {DataFormatter} from './DataFormatter';
|
import {DataFormatter} from './DataFormatter';
|
||||||
import {Layout} from './Layout';
|
import {Layout} from './Layout';
|
||||||
import {Typography} from 'antd';
|
import {Typography} from 'antd';
|
||||||
@@ -28,19 +21,13 @@ import {RightOutlined} from '@ant-design/icons';
|
|||||||
import {theme} from './theme';
|
import {theme} from './theme';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {DataTableManager} from './data-table/DataTableManager';
|
import {DataTableManager} from './data-table/DataTableManager';
|
||||||
import {Atom, createState} from '../state/atom';
|
|
||||||
import {useAssertStableRef} from '../utils/useAssertStableRef';
|
import {useAssertStableRef} from '../utils/useAssertStableRef';
|
||||||
import {DataSource} from '../data-source';
|
import {DataSource} from '../data-source';
|
||||||
|
import {useMakeStableCallback} from '../utils/useMakeStableCallback';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
interface Item {
|
type DataListBaseProps<Item> = {
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataListBaseProps<T extends Item> {
|
|
||||||
/**
|
/**
|
||||||
* Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop down
|
* Defines the styling of the component. By default shows a list, but alternatively the items can be displayed in a drop down
|
||||||
*/
|
*/
|
||||||
@@ -50,14 +37,11 @@ interface DataListBaseProps<T extends Item> {
|
|||||||
* By setting `scrollable={false}` the list will only take its natural size
|
* By setting `scrollable={false}` the list will only take its natural size
|
||||||
*/
|
*/
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
/**
|
|
||||||
* The current selection
|
|
||||||
*/
|
|
||||||
selection: Atom<string | undefined>;
|
|
||||||
/**
|
/**
|
||||||
* Handler that is fired if selection is changed
|
* Handler that is fired if selection is changed
|
||||||
*/
|
*/
|
||||||
onSelect?(id: string | undefined, value: T | undefined): void;
|
selection?: string | undefined;
|
||||||
|
onSelect?(id: string | undefined, value: Item | undefined): void;
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
/**
|
/**
|
||||||
@@ -67,85 +51,102 @@ interface DataListBaseProps<T extends Item> {
|
|||||||
/**
|
/**
|
||||||
* Custom render function. By default the component will render the `title` in bold and description (if any) below it
|
* Custom render function. By default the component will render the `title` in bold and description (if any) below it
|
||||||
*/
|
*/
|
||||||
onRenderItem?: ItemRenderer<T>;
|
onRenderItem?: ItemRenderer<Item>;
|
||||||
|
/**
|
||||||
|
* The attributes that will be picked as id/title/description for the default rendering.
|
||||||
|
* Defaults to id/title/description, but can be customized
|
||||||
|
*/
|
||||||
|
|
||||||
|
titleAttribute?: keyof Item & string;
|
||||||
|
descriptionAttribute?: keyof Item & string;
|
||||||
/**
|
/**
|
||||||
* Show a right arrow by default
|
* Show a right arrow by default
|
||||||
*/
|
*/
|
||||||
enableArrow?: boolean;
|
enableArrow?: boolean;
|
||||||
}
|
} & (Item extends {id: string}
|
||||||
|
? {
|
||||||
|
idAttribute?: keyof Item & string; // optional if id field is present
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
idAttribute: keyof Item & string;
|
||||||
|
});
|
||||||
|
|
||||||
export type DataListProps<T extends Item> = DataListBaseProps<T> &
|
export type DataListProps<Item> = DataListBaseProps<Item> &
|
||||||
// Some table props are set by DataList instead, so override them
|
// Some table props are set by DataList instead, so override them
|
||||||
Omit<DataTableProps<T>, 'records' | 'dataSource' | 'columns' | 'onSelect'>;
|
Omit<DataTableProps<Item>, 'records' | 'dataSource' | 'columns' | 'onSelect'>;
|
||||||
|
|
||||||
export const DataList: React.FC<DataListProps<any>> = function DataList<
|
export const DataList: (<T>(props: DataListProps<T>) => React.ReactElement) & {
|
||||||
T extends Item,
|
Item: React.FC<DataListItemProps>;
|
||||||
>({
|
} = Object.assign(
|
||||||
selection: baseSelection,
|
function <T>({
|
||||||
onSelect,
|
onSelect: baseOnSelect,
|
||||||
|
selection,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
items,
|
items,
|
||||||
onRenderItem,
|
onRenderItem,
|
||||||
enableArrow,
|
enableArrow,
|
||||||
|
idAttribute,
|
||||||
|
titleAttribute,
|
||||||
|
descriptionAttribute,
|
||||||
...tableProps
|
...tableProps
|
||||||
}: DataListProps<T>) {
|
}: DataListProps<T>) {
|
||||||
// if a tableManagerRef is provided, we piggy back on that same ref
|
// if a tableManagerRef is provided, we piggy back on that same ref
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const tableManagerRef = tableProps.tableManagerRef ?? createRef<undefined | DataTableManager<T>>();
|
const tableManagerRef = tableProps.tableManagerRef ?? createRef<undefined | DataTableManager<T>>();
|
||||||
|
|
||||||
useAssertStableRef(baseSelection, 'selection');
|
|
||||||
useAssertStableRef(onRenderItem, 'onRenderItem');
|
useAssertStableRef(onRenderItem, 'onRenderItem');
|
||||||
useAssertStableRef(enableArrow, 'enableArrow');
|
useAssertStableRef(enableArrow, 'enableArrow');
|
||||||
|
const onSelect = useMakeStableCallback(baseOnSelect);
|
||||||
// create local selection atom if none provided
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const selection = baseSelection ?? useState(() => createState<string|undefined>())[0];
|
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(item: T | undefined) => {
|
(item: T | undefined) => {
|
||||||
selection.set(item?.id);
|
if (!item) {
|
||||||
|
onSelect?.(undefined, undefined);
|
||||||
|
} else {
|
||||||
|
const id = '' + item[idAttribute!];
|
||||||
|
if (id == null) {
|
||||||
|
throw new Error(`No valid identifier for attribute ${idAttribute}`);
|
||||||
|
}
|
||||||
|
onSelect?.(id, item);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[selection],
|
[onSelect, idAttribute],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selection) {
|
||||||
|
tableManagerRef.current?.selectItemById(selection);
|
||||||
|
} else {
|
||||||
|
tableManagerRef.current?.clearSelection();
|
||||||
|
}
|
||||||
|
}, [selection, tableManagerRef]);
|
||||||
|
|
||||||
const dataListColumns: DataTableColumn<T>[] = useMemo(
|
const dataListColumns: DataTableColumn<T>[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
key: 'id' as const,
|
key: idAttribute!,
|
||||||
wrap: true,
|
wrap: true,
|
||||||
onRender(item: T, selected: boolean, index: number) {
|
onRender(item: T, selected: boolean, index: number) {
|
||||||
return onRenderItem ? (
|
return onRenderItem ? (
|
||||||
onRenderItem(item, selected, index)
|
onRenderItem(item, selected, index)
|
||||||
) : (
|
) : (
|
||||||
<DataListItem
|
<DataList.Item
|
||||||
title={item.title}
|
title={item[titleAttribute!] as any}
|
||||||
description={item.description}
|
description={item[descriptionAttribute!] as any}
|
||||||
enableArrow={enableArrow}
|
enableArrow={enableArrow}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[onRenderItem, enableArrow],
|
[
|
||||||
);
|
onRenderItem,
|
||||||
|
enableArrow,
|
||||||
useEffect(
|
titleAttribute,
|
||||||
function updateSelection() {
|
descriptionAttribute,
|
||||||
return selection.subscribe((valueFromAtom) => {
|
idAttribute,
|
||||||
const m = tableManagerRef.current;
|
],
|
||||||
if (!m) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!valueFromAtom && m.getSelectedItem()) {
|
|
||||||
m.clearSelection();
|
|
||||||
} else if (valueFromAtom && m.getSelectedItem()?.id !== valueFromAtom) {
|
|
||||||
// find valueFromAtom in the selection
|
|
||||||
m.selectItemById(valueFromAtom);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[selection, tableManagerRef],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -153,47 +154,33 @@ export const DataList: React.FC<DataListProps<any>> = function DataList<
|
|||||||
<DataTable<any>
|
<DataTable<any>
|
||||||
{...tableProps}
|
{...tableProps}
|
||||||
tableManagerRef={tableManagerRef}
|
tableManagerRef={tableManagerRef}
|
||||||
records={Array.isArray(items) ? items : undefined}
|
records={Array.isArray(items) ? items : undefined!}
|
||||||
dataSource={Array.isArray(items) ? undefined : (items as any)}
|
dataSource={Array.isArray(items) ? undefined! : (items as any)}
|
||||||
recordsKey="id"
|
recordsKey={idAttribute}
|
||||||
columns={dataListColumns}
|
columns={dataListColumns}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
/>
|
/>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
{
|
||||||
DataList.defaultProps = {
|
Item: memo(({title, description, enableArrow}: DataListItemProps) => {
|
||||||
type: 'default',
|
|
||||||
scrollable: true,
|
|
||||||
enableSearchbar: false,
|
|
||||||
enableColumnHeaders: false,
|
|
||||||
enableArrow: true,
|
|
||||||
enableContextMenu: false,
|
|
||||||
enableMultiSelect: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const DataListItem = memo(
|
|
||||||
({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
enableArrow,
|
|
||||||
}: {
|
|
||||||
// TODO: add icon support
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
enableArrow?: boolean;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Layout.Horizontal center grow shrink padv>
|
<Layout.Horizontal center grow shrink padv>
|
||||||
<Layout.Container grow shrink>
|
<Layout.Container grow shrink>
|
||||||
|
{typeof title === 'string' ? (
|
||||||
<Text strong ellipsis>
|
<Text strong ellipsis>
|
||||||
{DataFormatter.format(title)}
|
{DataFormatter.format(title)}
|
||||||
</Text>
|
</Text>
|
||||||
{description != null && (
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
|
{typeof description === 'string' ? (
|
||||||
<Text type="secondary" ellipsis>
|
<Text type="secondary" ellipsis>
|
||||||
{DataFormatter.format(description)}
|
{DataFormatter.format(description)}
|
||||||
</Text>
|
</Text>
|
||||||
|
) : (
|
||||||
|
description
|
||||||
)}
|
)}
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
{enableArrow && (
|
{enableArrow && (
|
||||||
@@ -203,9 +190,33 @@ const DataListItem = memo(
|
|||||||
)}
|
)}
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
);
|
);
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
(DataList as React.FC<DataListProps<any>>).defaultProps = {
|
||||||
|
type: 'default',
|
||||||
|
scrollable: true,
|
||||||
|
enableSearchbar: false,
|
||||||
|
enableColumnHeaders: false,
|
||||||
|
enableArrow: true,
|
||||||
|
enableContextMenu: false,
|
||||||
|
enableMultiSelect: false,
|
||||||
|
idAttribute: 'id',
|
||||||
|
titleAttribute: 'title',
|
||||||
|
descriptionAttribute: 'description',
|
||||||
|
};
|
||||||
|
(DataList.Item as React.FC<DataListItemProps>).defaultProps = {
|
||||||
|
enableArrow: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DataListItemProps {
|
||||||
|
// TODO: add icon support
|
||||||
|
title?: string | React.ReactElement;
|
||||||
|
description?: string | React.ReactElement;
|
||||||
|
enableArrow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const ArrowWrapper = styled.div({
|
const ArrowWrapper = styled.div({
|
||||||
flex: 0,
|
flex: 0,
|
||||||
paddingLeft: theme.space.small,
|
paddingLeft: theme.space.small,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ interface DataTableBaseProps<T = any> {
|
|||||||
enableColumnHeaders?: boolean;
|
enableColumnHeaders?: boolean;
|
||||||
enableMultiSelect?: boolean;
|
enableMultiSelect?: boolean;
|
||||||
enableContextMenu?: boolean;
|
enableContextMenu?: boolean;
|
||||||
|
enablePersistSettings?: boolean;
|
||||||
// if set (the default) will grow and become scrollable. Otherwise will use natural size
|
// if set (the default) will grow and become scrollable. Otherwise will use natural size
|
||||||
scrollable?: boolean;
|
scrollable?: boolean;
|
||||||
extraActions?: React.ReactElement;
|
extraActions?: React.ReactElement;
|
||||||
@@ -153,6 +154,7 @@ export function DataTable<T extends object>(
|
|||||||
scope,
|
scope,
|
||||||
virtualizerRef,
|
virtualizerRef,
|
||||||
autoScroll: props.enableAutoScroll,
|
autoScroll: props.enableAutoScroll,
|
||||||
|
enablePersistSettings: props.enablePersistSettings,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -177,15 +179,20 @@ export function DataTable<T extends object>(
|
|||||||
|
|
||||||
const renderingConfig = useMemo<TableRowRenderContext<T>>(() => {
|
const renderingConfig = useMemo<TableRowRenderContext<T>>(() => {
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns: visibleColumns,
|
columns: visibleColumns,
|
||||||
onMouseEnter(e, _item, index) {
|
onMouseEnter(e, _item, index) {
|
||||||
if (dragging.current && e.buttons === 1) {
|
if (dragging.current && e.buttons === 1 && props.enableMultiSelect) {
|
||||||
// by computing range we make sure no intermediate items are missed when scrolling fast
|
// by computing range we make sure no intermediate items are missed when scrolling fast
|
||||||
tableManager.addRangeToSelection(startIndex, index);
|
tableManager.addRangeToSelection(startIndex, index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMouseDown(e, _item, index) {
|
onMouseDown(e, _item, index) {
|
||||||
|
if (!props.enableMultiSelect && e.buttons > 1) {
|
||||||
|
tableManager.selectItem(index, false, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!dragging.current) {
|
if (!dragging.current) {
|
||||||
if (e.buttons > 1) {
|
if (e.buttons > 1) {
|
||||||
// for right click we only want to add if needed, not deselect
|
// for right click we only want to add if needed, not deselect
|
||||||
@@ -218,7 +225,13 @@ export function DataTable<T extends object>(
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
}, [visibleColumns, tableManager, onRowStyle, props.enableContextMenu]);
|
}, [
|
||||||
|
visibleColumns,
|
||||||
|
tableManager,
|
||||||
|
onRowStyle,
|
||||||
|
props.enableContextMenu,
|
||||||
|
props.enableMultiSelect,
|
||||||
|
]);
|
||||||
|
|
||||||
const itemRenderer = useCallback(
|
const itemRenderer = useCallback(
|
||||||
function itemRenderer(
|
function itemRenderer(
|
||||||
@@ -454,7 +467,7 @@ export function DataTable<T extends object>(
|
|||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
useRegex={tableState.useRegex}
|
useRegex={tableState.useRegex}
|
||||||
dispatch={dispatch as any}
|
dispatch={dispatch as any}
|
||||||
contextMenu={contexMenu}
|
contextMenu={props.enableContextMenu ? contexMenu : undefined}
|
||||||
extraActions={props.extraActions}
|
extraActions={props.extraActions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ type DataManagerConfig<T> = {
|
|||||||
onSelect: undefined | ((item: T | undefined, items: T[]) => void);
|
onSelect: undefined | ((item: T | undefined, items: T[]) => void);
|
||||||
virtualizerRef: MutableRefObject<DataSourceVirtualizer | undefined>;
|
virtualizerRef: MutableRefObject<DataSourceVirtualizer | undefined>;
|
||||||
autoScroll?: boolean;
|
autoScroll?: boolean;
|
||||||
|
enablePersistSettings?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DataManagerState<T> = {
|
type DataManagerState<T> = {
|
||||||
@@ -272,6 +273,7 @@ export type DataTableManager<T> = {
|
|||||||
toggleColumnVisibility(column: keyof T): void;
|
toggleColumnVisibility(column: keyof T): void;
|
||||||
sortColumn(column: keyof T, direction?: SortDirection): void;
|
sortColumn(column: keyof T, direction?: SortDirection): void;
|
||||||
setSearchValue(value: string): void;
|
setSearchValue(value: string): void;
|
||||||
|
dataSource: DataSource<T>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createDataTableManager<T>(
|
export function createDataTableManager<T>(
|
||||||
@@ -315,6 +317,7 @@ export function createDataTableManager<T>(
|
|||||||
setSearchValue(value) {
|
setSearchValue(value) {
|
||||||
dispatch({type: 'setSearchValue', value});
|
dispatch({type: 'setSearchValue', value});
|
||||||
},
|
},
|
||||||
|
dataSource,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +327,9 @@ export function createInitialState<T>(
|
|||||||
const storageKey = `${config.scope}:DataTable:${config.defaultColumns
|
const storageKey = `${config.scope}:DataTable:${config.defaultColumns
|
||||||
.map((c) => c.key)
|
.map((c) => c.key)
|
||||||
.join(',')}`;
|
.join(',')}`;
|
||||||
const prefs = loadStateFromStorage(storageKey);
|
const prefs = config.enablePersistSettings
|
||||||
|
? loadStateFromStorage(storageKey)
|
||||||
|
: undefined;
|
||||||
let initialColumns = computeInitialColumns(config.defaultColumns);
|
let initialColumns = computeInitialColumns(config.defaultColumns);
|
||||||
if (prefs) {
|
if (prefs) {
|
||||||
// merge prefs with the default column config
|
// merge prefs with the default column config
|
||||||
@@ -411,7 +416,7 @@ export function savePreferences(
|
|||||||
state: DataManagerState<any>,
|
state: DataManagerState<any>,
|
||||||
scrollOffset: number,
|
scrollOffset: number,
|
||||||
) {
|
) {
|
||||||
if (!state.config.scope) {
|
if (!state.config.scope || !state.config.enablePersistSettings) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const prefs: PersistedState = {
|
const prefs: PersistedState = {
|
||||||
|
|||||||
24
desktop/flipper-plugin/src/utils/useLatestRef.tsx
Normal file
24
desktop/flipper-plugin/src/utils/useLatestRef.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useEffect, useRef} from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ref object that is always synced from the value passed in.
|
||||||
|
*/
|
||||||
|
export function useLatestRef<T>(latest: T): {readonly current: T} {
|
||||||
|
const latestRef = useRef(latest);
|
||||||
|
|
||||||
|
// TODO: sync eagerly (in render) or late? Introduce a `syncEarly` flag as second arg
|
||||||
|
useEffect(() => {
|
||||||
|
latestRef.current = latest;
|
||||||
|
}, [latest]);
|
||||||
|
|
||||||
|
return latestRef;
|
||||||
|
}
|
||||||
34
desktop/flipper-plugin/src/utils/useMakeStableCallback.tsx
Normal file
34
desktop/flipper-plugin/src/utils/useMakeStableCallback.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useCallback} from 'react';
|
||||||
|
import {useLatestRef} from './useLatestRef';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hook can be used to avoid forcing consumers of a component to wrap their callbacks
|
||||||
|
* in useCallback, by creating wrapper callback that redirects to the lastest prop passed in.
|
||||||
|
*
|
||||||
|
* Use this hook if you would like to avoid that passing a new callback to this component,
|
||||||
|
* will cause child components to rerender when the callback is passed further down.
|
||||||
|
*
|
||||||
|
* Use it like: `const onSelect = useMakeStableCallback(props.onSelect)`.
|
||||||
|
* @param fn
|
||||||
|
*/
|
||||||
|
export function useMakeStableCallback<
|
||||||
|
T extends undefined | ((...args: any[]) => any),
|
||||||
|
>(fn: T): T {
|
||||||
|
const latestFn = useLatestRef(fn);
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
(...args: any[]) => {
|
||||||
|
return latestFn.current?.apply(null, args);
|
||||||
|
},
|
||||||
|
[latestFn],
|
||||||
|
) as any;
|
||||||
|
}
|
||||||
@@ -37,7 +37,7 @@ export function Crashes() {
|
|||||||
title: crash.reason ?? crash.name,
|
title: crash.reason ?? crash.name,
|
||||||
description: `${crash.date.toLocaleString()} - ${crash.name}`,
|
description: `${crash.date.toLocaleString()} - ${crash.name}`,
|
||||||
}))}
|
}))}
|
||||||
selection={plugin.selectedCrash}
|
selection={selectedCrashId}
|
||||||
onRenderEmpty={null}
|
onRenderEmpty={null}
|
||||||
/>
|
/>
|
||||||
{selectedCrash ? (
|
{selectedCrash ? (
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function ManageMockResponsePanel(props: Props) {
|
|||||||
</Toolbar>
|
</Toolbar>
|
||||||
<DataList
|
<DataList
|
||||||
items={items}
|
items={items}
|
||||||
selection={selectedIdAtom}
|
selection={selectedId}
|
||||||
onRenderItem={handleRender}
|
onRenderItem={handleRender}
|
||||||
scrollable
|
scrollable
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user