refactor(rtk-query): reduce selector computations

Other changes:

* simplify TreeView props
This commit is contained in:
FaberVitale 2021-06-24 18:13:59 +02:00
parent ad02c0ab26
commit 71b483bf02
11 changed files with 239 additions and 261 deletions

View File

@ -2,51 +2,101 @@ import React, { ReactNode } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { createTreeItemLabelRenderer } from '../styles/tree';
import {
QueryPreviewTabOption,
QueryPreviewTabs,
QueryPreviewTabProps,
QueryInfo,
SelectorsSource,
TabOption,
} from '../types';
import { QueryPreviewHeader } from './QueryPreviewHeader';
import { QueryPreviewInfo } from './QueryPreviewInfo';
import { QueryPreviewApi } from './QueryPreviewApi';
import { QueryPreviewSubscriptions } from './QueryPreviewSubscriptions';
import { QueryPreviewTags } from './QueryPreviewTags';
import { QueryPreviewInfo, QueryPreviewInfoProps } from './QueryPreviewInfo';
import { QueryPreviewApi, QueryPreviewApiProps } from './QueryPreviewApi';
import {
QueryPreviewSubscriptions,
QueryPreviewSubscriptionsProps,
} from './QueryPreviewSubscriptions';
import { QueryPreviewTags, QueryPreviewTagsProps } from './QueryPreviewTags';
import { NoRtkQueryApi } from './NoRtkQueryApi';
import { InspectorSelectors } from '../selectors';
import { StylingFunction } from 'react-base16-styling';
import { mapProps } from '../containers/mapProps';
export interface QueryPreviewProps
extends Omit<QueryPreviewTabProps, 'base16Theme' | 'invertTheme'> {
selectedTab: QueryPreviewTabs;
hasNoApis: boolean;
onTabChange: (tab: QueryPreviewTabs) => void;
export interface QueryPreviewProps<S = unknown> {
readonly selectedTab: QueryPreviewTabs;
readonly hasNoApis: boolean;
readonly onTabChange: (tab: QueryPreviewTabs) => void;
readonly queryInfo: QueryInfo | null;
readonly styling: StylingFunction;
readonly isWideLayout: boolean;
readonly selectorsSource: SelectorsSource<S>;
readonly selectors: InspectorSelectors<S>;
}
const tabs: ReadonlyArray<QueryPreviewTabOption> = [
/**
* Tab content is not rendered if there's no selected query.
*/
type QueryPreviewTabProps = Omit<QueryPreviewProps<unknown>, 'queryInfo'> & {
queryInfo: QueryInfo;
};
const MappedQueryPreviewTags = mapProps<
QueryPreviewTabProps,
QueryPreviewTagsProps
>(({ selectors, selectorsSource, isWideLayout, queryInfo }) => ({
queryInfo,
tags: selectors.selectCurrentQueryTags(selectorsSource),
isWideLayout,
}))(QueryPreviewTags);
const MappedQueryPreviewInfo = mapProps<
QueryPreviewTabProps,
QueryPreviewInfoProps
>(({ queryInfo, isWideLayout }) => ({ queryInfo, isWideLayout }))(
QueryPreviewInfo
);
const MappedQuerySubscriptipns = mapProps<
QueryPreviewTabProps,
QueryPreviewSubscriptionsProps
>(({ selectors, selectorsSource, isWideLayout }) => ({
isWideLayout,
subscriptions: selectors.selectSubscriptionsOfCurrentQuery(selectorsSource),
}))(QueryPreviewSubscriptions);
const MappedApiPreview = mapProps<QueryPreviewTabProps, QueryPreviewApiProps>(
({ isWideLayout, selectors, selectorsSource }) => ({
isWideLayout,
apiState: selectors.selectApiOfCurrentQuery(selectorsSource),
apiStats: selectors.selectApiStatsOfCurrentQuery(selectorsSource),
})
)(QueryPreviewApi);
const tabs: ReadonlyArray<TabOption<QueryPreviewTabs, QueryPreviewTabProps>> = [
{
label: 'query',
value: QueryPreviewTabs.queryinfo,
component: QueryPreviewInfo,
component: MappedQueryPreviewInfo,
},
{
label: 'tags',
value: QueryPreviewTabs.queryTags,
component: QueryPreviewTags,
component: MappedQueryPreviewTags,
},
{
label: 'subs',
value: QueryPreviewTabs.querySubscriptions,
component: QueryPreviewSubscriptions,
component: MappedQuerySubscriptipns,
},
{
label: 'api',
value: QueryPreviewTabs.apiConfig,
component: QueryPreviewApi,
component: MappedApiPreview,
},
];
export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
export class QueryPreview<S> extends React.PureComponent<QueryPreviewProps<S>> {
readonly labelRenderer: ReturnType<typeof createTreeItemLabelRenderer>;
constructor(props: QueryPreviewProps) {
constructor(props: QueryPreviewProps<S>) {
super(props);
this.labelRenderer = createTreeItemLabelRenderer(this.props.styling);
@ -65,40 +115,19 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
return `${label} (${counterAsString})`;
};
renderTabLabel = (tab: QueryPreviewTabOption): ReactNode => {
const { queryInfo, tags, querySubscriptions } = this.props;
if (queryInfo) {
if (tab.value === QueryPreviewTabs.queryTags && tags.length > 0) {
return this.renderLabelWithCounter(tab.label, tags.length);
}
renderTabLabel = (tab: TabOption<QueryPreviewTabs, unknown>): ReactNode => {
const { selectors, selectorsSource } = this.props;
const tabCount = selectors.selectTabCounters(selectorsSource)[tab.value];
if (
tab.value === QueryPreviewTabs.querySubscriptions &&
querySubscriptions
) {
const subsCount = Object.keys(querySubscriptions).length;
if (subsCount > 0) {
return this.renderLabelWithCounter(tab.label, subsCount);
}
}
if (tabCount > 0) {
return this.renderLabelWithCounter(tab.label, tabCount);
}
return tab.label;
};
render(): ReactNode {
const {
queryInfo,
isWideLayout,
selectedTab,
apiState,
onTabChange,
querySubscriptions,
tags,
apiStats,
hasNoApis,
} = this.props;
const { queryInfo, selectedTab, onTabChange, hasNoApis } = this.props;
const { component: TabComponent } =
tabs.find((tab) => tab.value === selectedTab) || tabs[0];
@ -111,7 +140,9 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
<QueryPreviewHeader
selectedTab={selectedTab}
onTabChange={onTabChange}
tabs={tabs}
tabs={
tabs as ReadonlyArray<TabOption<QueryPreviewTabs, unknown>>
}
renderTabLabel={this.renderTabLabel}
/>
{hasNoApis && <NoRtkQueryApi />}
@ -123,26 +154,18 @@ export class QueryPreview extends React.PureComponent<QueryPreviewProps> {
return (
<StyleUtilsContext.Consumer>
{({ styling, base16Theme, invertTheme }) => {
{({ styling }) => {
return (
<div {...styling('queryPreview')}>
<QueryPreviewHeader
selectedTab={selectedTab}
onTabChange={onTabChange}
tabs={tabs}
tabs={
tabs as ReadonlyArray<TabOption<QueryPreviewTabs, unknown>>
}
renderTabLabel={this.renderTabLabel}
/>
<TabComponent
styling={styling}
base16Theme={base16Theme}
invertTheme={invertTheme}
querySubscriptions={querySubscriptions}
queryInfo={queryInfo}
tags={tags}
apiState={apiState}
isWideLayout={isWideLayout}
apiStats={apiStats}
/>
<TabComponent {...(this.props as QueryPreviewTabProps)} />
</div>
);
}}

View File

@ -1,18 +1,19 @@
import { createSelector } from '@reduxjs/toolkit';
import React, { ReactNode, PureComponent } from 'react';
import { QueryPreviewTabProps } from '../types';
import { ApiStats, RtkQueryApiState } from '../types';
import { TreeView } from './TreeView';
interface TreeDisplayed {
reducerPath: string;
api: QueryPreviewTabProps['apiState'];
stats: QueryPreviewTabProps['apiStats'];
export interface QueryPreviewApiProps {
apiStats: ApiStats | null;
apiState: RtkQueryApiState | null;
isWideLayout: boolean;
}
export class QueryPreviewApi extends PureComponent<QueryPreviewTabProps> {
export class QueryPreviewApi extends PureComponent<QueryPreviewApiProps> {
selectData = createSelector(
[
({ apiState }: QueryPreviewTabProps) => apiState,
({ apiStats }: QueryPreviewTabProps) => apiStats,
({ apiState }: QueryPreviewApiProps) => apiState,
({ apiStats }: QueryPreviewApiProps) => apiStats,
],
(apiState, apiStats) => ({
reducerPath: apiState?.config?.reducerPath ?? null,
@ -22,20 +23,10 @@ export class QueryPreviewApi extends PureComponent<QueryPreviewTabProps> {
);
render(): ReactNode {
const { queryInfo, isWideLayout, base16Theme, styling, invertTheme } =
this.props;
if (!queryInfo) {
return null;
}
return (
<TreeView
data={this.selectData(this.props)}
isWideLayout={isWideLayout}
base16Theme={base16Theme}
styling={styling}
invertTheme={invertTheme}
isWideLayout={this.props.isWideLayout}
/>
);
}

View File

@ -1,17 +1,17 @@
import React, { ReactNode } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { QueryPreviewTabOption, QueryPreviewTabs } from '../types';
import { QueryPreviewTabs, TabOption } from '../types';
import { emptyArray } from '../utils/object';
export interface QueryPreviewHeaderProps {
tabs: ReadonlyArray<QueryPreviewTabOption>;
tabs: ReadonlyArray<TabOption<QueryPreviewTabs, unknown>>;
onTabChange: (tab: QueryPreviewTabs) => void;
selectedTab: QueryPreviewTabs;
renderTabLabel?: (tab: QueryPreviewTabOption) => ReactNode;
renderTabLabel?: (tab: QueryPreviewHeaderProps['tabs'][number]) => ReactNode;
}
export class QueryPreviewHeader extends React.Component<QueryPreviewHeaderProps> {
handleTabClick = (tab: QueryPreviewTabOption): void => {
handleTabClick = (tab: QueryPreviewHeaderProps['tabs'][number]): void => {
if (this.props.selectedTab !== tab.value) {
this.props.onTabChange(tab.value);
}

View File

@ -1,11 +1,6 @@
import { createSelector, Selector } from '@reduxjs/toolkit';
import React, { ReactNode, PureComponent } from 'react';
import {
QueryInfo,
QueryPreviewTabProps,
RtkQueryState,
RTKStatusFlags,
} from '../types';
import { QueryInfo, RtkQueryState, RTKStatusFlags } from '../types';
import { identity } from '../utils/object';
import { getQueryStatusFlags } from '../utils/rtk-query';
import { TreeView } from './TreeView';
@ -22,17 +17,14 @@ interface FormattedQuery extends ComputedQueryInfo {
query: RtkQueryState;
}
export class QueryPreviewInfo extends PureComponent<QueryPreviewTabProps> {
selectFormattedQuery: Selector<
QueryPreviewTabProps['queryInfo'],
FormattedQuery | null
> = createSelector(
export interface QueryPreviewInfoProps {
queryInfo: QueryInfo;
isWideLayout: boolean;
}
export class QueryPreviewInfo extends PureComponent<QueryPreviewInfoProps> {
selectFormattedQuery: Selector<QueryInfo, FormattedQuery> = createSelector(
identity,
(queryInfo: QueryInfo | null): FormattedQuery | null => {
if (!queryInfo) {
return null;
}
(queryInfo: QueryInfo): FormattedQuery => {
const { query, queryKey, reducerPath } = queryInfo;
const startedAt = query.startedTimeStamp
@ -57,23 +49,9 @@ export class QueryPreviewInfo extends PureComponent<QueryPreviewTabProps> {
);
render(): ReactNode {
const { queryInfo, isWideLayout, base16Theme, styling, invertTheme } =
this.props;
const { queryInfo, isWideLayout } = this.props;
const formattedQuery = this.selectFormattedQuery(queryInfo);
if (!formattedQuery) {
return null;
}
return (
<TreeView
data={formattedQuery}
isWideLayout={isWideLayout}
base16Theme={base16Theme}
styling={styling}
invertTheme={invertTheme}
/>
);
return <TreeView data={formattedQuery} isWideLayout={isWideLayout} />;
}
}

View File

@ -1,60 +1,18 @@
import React, { ReactNode, PureComponent } from 'react';
import { QueryPreviewTabProps } from '../types';
import { RtkQueryApiState } from '../types';
import { TreeView } from './TreeView';
export interface QueryPreviewSubscriptionsState {
data: { subscriptions: QueryPreviewTabProps['querySubscriptions'] };
export interface QueryPreviewSubscriptionsProps {
subscriptions: RtkQueryApiState['subscriptions'][keyof RtkQueryApiState['subscriptions']];
isWideLayout: boolean;
}
export class QueryPreviewSubscriptions extends PureComponent<
QueryPreviewTabProps,
QueryPreviewSubscriptionsState
> {
static getDerivedStateFromProps(
props: QueryPreviewTabProps,
state: QueryPreviewSubscriptionsState
): Partial<QueryPreviewSubscriptionsState> | null {
if (props.querySubscriptions !== state.data.subscriptions) {
return {
data: { subscriptions: props.querySubscriptions },
};
}
return null;
}
constructor(props: QueryPreviewTabProps) {
super(props);
this.state = {
data: { subscriptions: props.querySubscriptions },
};
}
export class QueryPreviewSubscriptions extends PureComponent<QueryPreviewSubscriptionsProps> {
render(): ReactNode {
const {
queryInfo,
isWideLayout,
base16Theme,
styling,
invertTheme,
querySubscriptions,
} = this.props;
if (!querySubscriptions || !queryInfo) {
return null;
}
const { subscriptions } = this.props;
return (
<>
<TreeView
data={this.state.data}
isWideLayout={isWideLayout}
base16Theme={base16Theme}
styling={styling}
invertTheme={invertTheme}
/>
</>
<TreeView data={subscriptions} isWideLayout={this.props.isWideLayout} />
);
}
}

View File

@ -1,29 +1,21 @@
import React, { ReactNode, PureComponent } from 'react';
import { QueryPreviewTabProps } from '../types';
import { RtkQueryTag } from '../types';
import { TreeView } from './TreeView';
interface QueryPreviewTagsState {
data: { tags: QueryPreviewTabProps['tags'] };
data: { tags: RtkQueryTag[] };
}
export interface QueryPreviewTagsProps {
tags: RtkQueryTag[];
isWideLayout: boolean;
}
export class QueryPreviewTags extends PureComponent<
QueryPreviewTabProps,
QueryPreviewTagsProps,
QueryPreviewTagsState
> {
static getDerivedStateFromProps(
{ tags }: QueryPreviewTabProps,
state: QueryPreviewTagsState
): QueryPreviewTagsState | null {
if (tags !== state.data.tags) {
return {
data: { tags },
};
}
return null;
}
constructor(props: QueryPreviewTabProps) {
constructor(props: QueryPreviewTagsProps) {
super(props);
this.state = {
@ -32,21 +24,8 @@ export class QueryPreviewTags extends PureComponent<
}
render(): ReactNode {
const { queryInfo, isWideLayout, base16Theme, styling, invertTheme } =
this.props;
const { isWideLayout, tags } = this.props;
if (!queryInfo) {
return null;
}
return (
<TreeView
data={this.state.data}
isWideLayout={isWideLayout}
base16Theme={base16Theme}
styling={styling}
invertTheme={invertTheme}
/>
);
return <TreeView data={tags} isWideLayout={isWideLayout} />;
}
}

View File

@ -1,11 +1,16 @@
import { createSelector } from '@reduxjs/toolkit';
import React, { ComponentProps, ReactNode } from 'react';
import JSONTree from 'react-json-tree';
import { StylingFunction } from 'react-base16-styling';
import { DATA_TYPE_KEY } from '../monitor-config';
import { getJsonTreeTheme } from '../styles/createStylingFromTheme';
import {
getJsonTreeTheme,
StyleUtilsContext,
} from '../styles/createStylingFromTheme';
import { createTreeItemLabelRenderer, getItemString } from '../styles/tree';
import { StyleUtils } from '../types';
import { identity } from '../utils/object';
export interface TreeViewProps extends StyleUtils {
export interface TreeViewProps {
data: unknown;
isWideLayout: boolean;
before?: ReactNode;
@ -15,37 +20,39 @@ export interface TreeViewProps extends StyleUtils {
}
export class TreeView extends React.PureComponent<TreeViewProps> {
readonly labelRenderer: ReturnType<typeof createTreeItemLabelRenderer>;
readonly selectLabelRenderer = createSelector<
StylingFunction,
StylingFunction,
ReturnType<typeof createTreeItemLabelRenderer>
>(identity, createTreeItemLabelRenderer);
constructor(props: TreeViewProps) {
super(props);
this.labelRenderer = createTreeItemLabelRenderer(this.props.styling);
}
render(): ReactNode {
const {
styling,
base16Theme,
invertTheme,
isWideLayout,
data,
before,
after,
children,
keyPath,
} = this.props;
const { isWideLayout, data, before, after, children, keyPath } = this.props;
return (
<StyleUtilsContext.Consumer>
{({ styling, invertTheme, base16Theme }) => {
return (
<div {...styling('treeWrapper')}>
{before}
<JSONTree
keyPath={keyPath}
data={data}
labelRenderer={this.labelRenderer}
labelRenderer={this.selectLabelRenderer(styling)}
theme={getJsonTreeTheme(base16Theme)}
invertTheme={invertTheme}
getItemString={(type, data) =>
getItemString(styling, type, data, DATA_TYPE_KEY, isWideLayout)
getItemString(
styling,
type,
data,
DATA_TYPE_KEY,
isWideLayout
)
}
hideRoot
/>
@ -53,5 +60,8 @@ export class TreeView extends React.PureComponent<TreeViewProps> {
{children}
</div>
);
}}
</StyleUtilsContext.Consumer>
);
}
}

View File

@ -18,7 +18,6 @@ import {
import { QueryList } from '../components/QueryList';
import { QueryForm } from '../components/QueryForm';
import { QueryPreview } from '../components/QueryPreview';
import { getApiStateOf, getQuerySubscriptionsOf } from '../utils/rtk-query';
type ForwardedMonitorProps<S, A extends Action<unknown>> = Pick<
LiftedState<S, A, RtkQueryMonitorState>,
@ -115,26 +114,15 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
const {
styleUtils: { styling },
} = this.props;
const apiStates = this.selectors.selectApiStates(selectorsSource);
const allVisibleQueries =
this.selectors.selectAllVisbileQueries(selectorsSource);
const currentQueryInfo =
this.selectors.selectCurrentQueryInfo(selectorsSource);
const currentRtkApi =
this.selectors.selectApiOfCurrentQuery(selectorsSource);
const currentQuerySubscriptions = getQuerySubscriptionsOf(
currentQueryInfo,
apiStates
);
const apiStates = this.selectors.selectApiStates(selectorsSource);
const currentTags = this.selectors.selectCurrentQueryTags(selectorsSource);
const currentApiStats =
this.selectors.selectApiStatsOfCurrentQuery(selectorsSource);
const hasNoApis = apiStates == null;
const hasNoApi = apiStates == null;
return (
<div
@ -156,17 +144,15 @@ class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
selectedQueryKey={selectorsSource.monitorState.selectedQueryKey}
/>
</div>
<QueryPreview
<QueryPreview<S>
selectorsSource={this.state.selectorsSource}
selectors={this.selectors}
queryInfo={currentQueryInfo}
selectedTab={selectorsSource.monitorState.selectedPreviewTab}
onTabChange={this.handleTabChange}
styling={styling}
tags={currentTags}
querySubscriptions={currentQuerySubscriptions}
apiState={currentRtkApi}
isWideLayout={isWideLayout}
apiStats={currentApiStats}
hasNoApis={hasNoApis}
hasNoApis={hasNoApi}
/>
</div>
);

View File

@ -0,0 +1,29 @@
import React, { ComponentType, ReactNode, Component } from 'react';
interface Mapper<In, Out> {
(inProps: In): Out;
}
interface MapPropsOutput<In, Out> {
(comp: ComponentType<Out>): ComponentType<In>;
}
export function mapProps<In, Out>(
mapper: Mapper<In, Out>
): MapPropsOutput<In, Out> {
return function mapPropsHoc(Comp) {
class MapPropsHoc extends Component<In> {
render(): ReactNode {
const mappedProps = mapper(this.props);
return <Comp {...mappedProps} />;
}
static displayName = `mapProps(${
Comp.displayName || Comp.name || 'Component'
})`;
}
return MapPropsHoc;
};
}

View File

@ -7,9 +7,11 @@ import {
RtkQueryTag,
SelectorsSource,
RtkQueryProvided,
QueryPreviewTabs,
} from './types';
import { Comparator, queryComparators } from './utils/comparators';
import { FilterList, queryListFilters } from './utils/filters';
import { emptyRecord } from './utils/object';
import { escapeRegExpSpecialCharacter } from './utils/regexp';
import {
getApiStatesOf,
@ -63,6 +65,14 @@ export interface InspectorSelectors<S> {
S,
RtkQueryApiState | null
>;
readonly selectTabCounters: InspectorSelector<
S,
Record<QueryPreviewTabs, number>
>;
readonly selectSubscriptionsOfCurrentQuery: InspectorSelector<
S,
RtkQueryApiState['subscriptions'][string]
>;
}
export function createInspectorSelectors<S>(): InspectorSelectors<S> {
@ -161,6 +171,17 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
return selectApiOfCurrentQuery(selectorsSource)?.provided ?? null;
};
const selectSubscriptionsOfCurrentQuery = createSelector(
[selectApiOfCurrentQuery, selectCurrentQueryInfo],
(apiState, queryInfo) => {
if (!queryInfo || !apiState) {
return emptyRecord;
}
return apiState.subscriptions[queryInfo.queryKey];
}
);
const selectCurrentQueryTags = createSelector(
[selectCurrentQueryInfo, selectProvidedOfCurrentQuery],
getQueryTagsOf
@ -171,6 +192,21 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
generateApiStatsOfCurrentQuery
);
const selectTabCounters = (
selectorsSource: SelectorsSource<S>
): Record<QueryPreviewTabs, number> => {
const subscriptions = selectSubscriptionsOfCurrentQuery(selectorsSource);
const subsLen = Object.keys(subscriptions ?? {}).length;
return {
[QueryPreviewTabs.queryTags]:
selectCurrentQueryTags(selectorsSource).length,
[QueryPreviewTabs.querySubscriptions]: subsLen,
[QueryPreviewTabs.apiConfig]: 0,
[QueryPreviewTabs.queryinfo]: 0,
};
};
return {
selectQueryComparator,
selectApiStates,
@ -180,6 +216,8 @@ export function createInspectorSelectors<S>(): InspectorSelectors<S> {
selectCurrentQueryInfo,
selectCurrentQueryTags,
selectApiStatsOfCurrentQuery,
selectSubscriptionsOfCurrentQuery,
selectApiOfCurrentQuery,
selectTabCounters,
};
}

View File

@ -132,24 +132,10 @@ export interface ApiStats {
}>;
}
export interface QueryPreviewTabProps extends StyleUtils {
queryInfo: QueryInfo | null;
apiState: RtkQueryApiState | null;
querySubscriptions: RTKQuerySubscribers | null;
isWideLayout: boolean;
tags: RtkQueryTag[];
apiStats: ApiStats | null;
}
export interface TabOption<S, P> extends SelectOption<S> {
component: ComponentType<P>;
}
export type QueryPreviewTabOption = TabOption<
QueryPreviewTabs,
QueryPreviewTabProps
>;
/**
* It is Omit<RequestStatusFlags, 'status'> & { isFetching: boolean; }
*/