mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[embeddable refactor] publishing subject cleanup (#179384)
Publishing subject cleanup 1. add `skip` operator to PublishingSubject subscriptions to avoid subscriptions from immediately firing with current value. From [RXJS docs](https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject), " when a new observer subscribes to a BehaviorSubject, it immediately receives the current value (or the last value that was emitted)." 2. Remove wrapper methods around `useStateFromPublishingSubject`. `useStateFromPublishingSubject` should just be used directly. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
176ea275a8
commit
d54200bc42
14 changed files with 248 additions and 231 deletions
|
@ -9,7 +9,6 @@
|
|||
export {
|
||||
apiCanDuplicatePanels,
|
||||
apiCanExpandPanels,
|
||||
useExpandedPanelId,
|
||||
type CanDuplicatePanels,
|
||||
type CanExpandPanels,
|
||||
} from './interfaces/panel_management';
|
||||
|
|
|
@ -6,10 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
PublishingSubject,
|
||||
useStateFromPublishingSubject,
|
||||
} from '@kbn/presentation-publishing/publishing_subject';
|
||||
import { PublishingSubject } from '@kbn/presentation-publishing/publishing_subject';
|
||||
|
||||
export interface CanDuplicatePanels {
|
||||
duplicatePanel: (panelId: string) => void;
|
||||
|
@ -29,9 +26,3 @@ export interface CanExpandPanels {
|
|||
export const apiCanExpandPanels = (unknownApi: unknown | null): unknownApi is CanExpandPanels => {
|
||||
return Boolean((unknownApi as CanExpandPanels)?.expandPanel !== undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets this API's expanded panel state as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useExpandedPanelId = (api: Partial<CanExpandPanels> | undefined) =>
|
||||
useStateFromPublishingSubject(apiCanExpandPanels(api) ? api.expandedPanelId : undefined);
|
||||
|
|
|
@ -44,22 +44,15 @@ export {
|
|||
export { apiHasUniqueId, type HasUniqueId } from './interfaces/has_uuid';
|
||||
export {
|
||||
apiPublishesBlockingError,
|
||||
useBlockingError,
|
||||
type PublishesBlockingError,
|
||||
} from './interfaces/publishes_blocking_error';
|
||||
export {
|
||||
apiPublishesDataLoading,
|
||||
useDataLoading,
|
||||
type PublishesDataLoading,
|
||||
} from './interfaces/publishes_data_loading';
|
||||
export {
|
||||
apiPublishesDataViews,
|
||||
useDataViews,
|
||||
type PublishesDataViews,
|
||||
} from './interfaces/publishes_data_views';
|
||||
export { apiPublishesDataViews, type PublishesDataViews } from './interfaces/publishes_data_views';
|
||||
export {
|
||||
apiPublishesDisabledActionIds,
|
||||
useDisabledActionIds,
|
||||
type PublishesDisabledActionIds,
|
||||
} from './interfaces/publishes_disabled_action_ids';
|
||||
export {
|
||||
|
@ -80,18 +73,15 @@ export {
|
|||
export { initializeTimeRange } from './interfaces/unified_search/initialize_time_range';
|
||||
export {
|
||||
apiPublishesSavedObjectId,
|
||||
useSavedObjectId,
|
||||
type PublishesSavedObjectId,
|
||||
} from './interfaces/publishes_saved_object_id';
|
||||
export {
|
||||
apiPublishesUnsavedChanges,
|
||||
useUnsavedChanges,
|
||||
type PublishesUnsavedChanges,
|
||||
} from './interfaces/publishes_unsaved_changes';
|
||||
export {
|
||||
apiPublishesViewMode,
|
||||
apiPublishesWritableViewMode,
|
||||
useViewMode,
|
||||
type PublishesViewMode,
|
||||
type PublishesWritableViewMode,
|
||||
type ViewMode,
|
||||
|
@ -99,8 +89,6 @@ export {
|
|||
export {
|
||||
apiPublishesPanelDescription,
|
||||
apiPublishesWritablePanelDescription,
|
||||
useDefaultPanelDescription,
|
||||
usePanelDescription,
|
||||
type PublishesPanelDescription,
|
||||
type PublishesWritablePanelDescription,
|
||||
} from './interfaces/titles/publishes_panel_description';
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
export interface PublishesBlockingError {
|
||||
blockingError: PublishingSubject<Error | undefined>;
|
||||
|
@ -17,9 +17,3 @@ export const apiPublishesBlockingError = (
|
|||
): unknownApi is PublishesBlockingError => {
|
||||
return Boolean(unknownApi && (unknownApi as PublishesBlockingError)?.blockingError !== undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets this API's fatal error as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useBlockingError = (api: Partial<PublishesBlockingError> | undefined) =>
|
||||
useStateFromPublishingSubject(api?.blockingError);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
export interface PublishesDataLoading {
|
||||
dataLoading: PublishingSubject<boolean | undefined>;
|
||||
|
@ -17,9 +17,3 @@ export const apiPublishesDataLoading = (
|
|||
): unknownApi is PublishesDataLoading => {
|
||||
return Boolean(unknownApi && (unknownApi as PublishesDataLoading)?.dataLoading !== undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets this API's data loading state as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useDataLoading = (api: Partial<PublishesDataLoading> | undefined) =>
|
||||
useStateFromPublishingSubject(apiPublishesDataLoading(api) ? api.dataLoading : undefined);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
export interface PublishesDataViews {
|
||||
dataViews: PublishingSubject<DataView[] | undefined>;
|
||||
|
@ -18,9 +18,3 @@ export const apiPublishesDataViews = (
|
|||
): unknownApi is PublishesDataViews => {
|
||||
return Boolean(unknownApi && (unknownApi as PublishesDataViews)?.dataViews !== undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets this API's data views as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useDataViews = (api: Partial<PublishesDataViews> | undefined) =>
|
||||
useStateFromPublishingSubject(apiPublishesDataViews(api) ? api.dataViews : undefined);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
export interface PublishesDisabledActionIds {
|
||||
disabledActionIds: PublishingSubject<string[] | undefined>;
|
||||
|
@ -24,9 +24,3 @@ export const apiPublishesDisabledActionIds = (
|
|||
unknownApi && (unknownApi as PublishesDisabledActionIds)?.disabledActionIds !== undefined
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets this API's disabled action IDs as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useDisabledActionIds = (api: Partial<PublishesDisabledActionIds> | undefined) =>
|
||||
useStateFromPublishingSubject(api?.disabledActionIds);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
/**
|
||||
* This API publishes a saved object id which can be used to determine which saved object this API is linked to.
|
||||
|
@ -23,9 +23,3 @@ export const apiPublishesSavedObjectId = (
|
|||
): unknownApi is PublishesSavedObjectId => {
|
||||
return Boolean(unknownApi && (unknownApi as PublishesSavedObjectId)?.savedObjectId !== undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that gets this API's saved object ID as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useSavedObjectId = (api: PublishesSavedObjectId | undefined) =>
|
||||
useStateFromPublishingSubject(api?.savedObjectId);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
export interface PublishesUnsavedChanges {
|
||||
unsavedChanges: PublishingSubject<object | undefined>;
|
||||
|
@ -20,9 +20,3 @@ export const apiPublishesUnsavedChanges = (api: unknown): api is PublishesUnsave
|
|||
(api as PublishesUnsavedChanges).resetUnsavedChanges
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that gets this API's unsaved changes as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useUnsavedChanges = (api: PublishesUnsavedChanges | undefined) =>
|
||||
useStateFromPublishingSubject(api?.unsavedChanges);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../publishing_subject';
|
||||
import { PublishingSubject } from '../publishing_subject';
|
||||
|
||||
export type ViewMode = 'view' | 'edit' | 'print' | 'preview';
|
||||
|
||||
|
@ -44,12 +44,3 @@ export const apiPublishesWritableViewMode = (
|
|||
typeof (unknownApi as PublishesWritableViewMode).setViewMode === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that gets this API's view mode as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useViewMode = <
|
||||
ApiType extends Partial<PublishesViewMode> = Partial<PublishesViewMode>
|
||||
>(
|
||||
api: ApiType | undefined
|
||||
) => useStateFromPublishingSubject(api?.viewMode);
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { PublishingSubject, useStateFromPublishingSubject } from '../../publishing_subject';
|
||||
import { PublishingSubject } from '../../publishing_subject';
|
||||
|
||||
export interface PublishesPanelDescription {
|
||||
panelDescription: PublishingSubject<string | undefined>;
|
||||
|
@ -34,15 +34,3 @@ export const apiPublishesWritablePanelDescription = (
|
|||
typeof (unknownApi as PublishesWritablePanelDescription).setPanelDescription === 'function'
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that gets this API's panel description as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const usePanelDescription = (api: Partial<PublishesPanelDescription> | undefined) =>
|
||||
useStateFromPublishingSubject(api?.panelDescription);
|
||||
|
||||
/**
|
||||
* A hook that gets this API's default panel description as a reactive variable which will cause re-renders on change.
|
||||
*/
|
||||
export const useDefaultPanelDescription = (api: Partial<PublishesPanelDescription> | undefined) =>
|
||||
useStateFromPublishingSubject(api?.defaultPanelDescription);
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { debounceTime, skip } from 'rxjs/operators';
|
||||
import { AnyPublishingSubject, PublishingSubject, UnwrapPublishingSubjectTuple } from './types';
|
||||
|
||||
const hasSubjectsArrayChanged = (
|
||||
|
@ -27,10 +27,13 @@ const hasSubjectsArrayChanged = (
|
|||
/**
|
||||
* Batches the latest values of multiple publishing subjects into a single object. Use this to avoid unnecessary re-renders.
|
||||
* You should avoid using this hook with subjects that your component pushes values to on user interaction, as it can cause a slight delay.
|
||||
* @param subjects Publishing subjects array.
|
||||
* When 'subjects' is expected to change, 'subjects' must be part of component react state.
|
||||
*/
|
||||
export const useBatchedPublishingSubjects = <SubjectsType extends [...AnyPublishingSubject[]]>(
|
||||
...subjects: [...SubjectsType]
|
||||
): UnwrapPublishingSubjectTuple<SubjectsType> => {
|
||||
const isFirstRender = useRef(true);
|
||||
/**
|
||||
* memoize and deep diff subjects to avoid rebuilding the subscription when the subjects are the same.
|
||||
*/
|
||||
|
@ -46,17 +49,20 @@ export const useBatchedPublishingSubjects = <SubjectsType extends [...AnyPublish
|
|||
/**
|
||||
* Set up latest published values state, initialized with the current values of the subjects.
|
||||
*/
|
||||
const initialSubjectValues = useMemo(
|
||||
() => unwrapPublishingSubjectArray(subjectsToUse),
|
||||
[subjectsToUse]
|
||||
);
|
||||
const [latestPublishedValues, setLatestPublishedValues] =
|
||||
useState<UnwrapPublishingSubjectTuple<SubjectsType>>(initialSubjectValues);
|
||||
const [latestPublishedValues, setLatestPublishedValues] = useState<
|
||||
UnwrapPublishingSubjectTuple<SubjectsType>
|
||||
>(() => unwrapPublishingSubjectArray(subjectsToUse));
|
||||
|
||||
/**
|
||||
* Subscribe to all subjects and update the latest values when any of them change.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isFirstRender.current) {
|
||||
setLatestPublishedValues(unwrapPublishingSubjectArray(subjectsToUse));
|
||||
} else {
|
||||
isFirstRender.current = false;
|
||||
}
|
||||
|
||||
const definedSubjects: Array<PublishingSubject<unknown>> = [];
|
||||
const definedSubjectIndices: number[] = [];
|
||||
|
||||
|
@ -67,7 +73,11 @@ export const useBatchedPublishingSubjects = <SubjectsType extends [...AnyPublish
|
|||
}
|
||||
if (definedSubjects.length === 0) return;
|
||||
const subscription = combineLatest(definedSubjects)
|
||||
.pipe(debounceTime(0))
|
||||
.pipe(
|
||||
debounceTime(0),
|
||||
// When a new observer subscribes to a BehaviorSubject, it immediately receives the current value. Skip this emit.
|
||||
skip(1)
|
||||
)
|
||||
.subscribe((values) => {
|
||||
setLatestPublishedValues((lastPublishedValues) => {
|
||||
const newLatestPublishedValues: UnwrapPublishingSubjectTuple<SubjectsType> = [
|
||||
|
|
|
@ -6,150 +6,227 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useBatchedPublishingSubjects } from './publishing_batcher';
|
||||
import { useStateFromPublishingSubject } from './publishing_subject';
|
||||
import { PublishingSubject } from './types';
|
||||
|
||||
describe('useBatchedPublishingSubjects', () => {
|
||||
let subject1: BehaviorSubject<number>;
|
||||
let subject2: BehaviorSubject<number>;
|
||||
let subject3: BehaviorSubject<number>;
|
||||
let subject4: BehaviorSubject<number>;
|
||||
let subject5: BehaviorSubject<number>;
|
||||
let subject6: BehaviorSubject<number>;
|
||||
beforeEach(() => {
|
||||
subject1 = new BehaviorSubject<number>(0);
|
||||
subject2 = new BehaviorSubject<number>(0);
|
||||
subject3 = new BehaviorSubject<number>(0);
|
||||
subject4 = new BehaviorSubject<number>(0);
|
||||
subject5 = new BehaviorSubject<number>(0);
|
||||
subject6 = new BehaviorSubject<number>(0);
|
||||
describe('render', () => {
|
||||
let subject1: BehaviorSubject<number>;
|
||||
let subject2: BehaviorSubject<number>;
|
||||
let subject3: BehaviorSubject<number>;
|
||||
let subject4: BehaviorSubject<number>;
|
||||
let subject5: BehaviorSubject<number>;
|
||||
let subject6: BehaviorSubject<number>;
|
||||
beforeEach(() => {
|
||||
subject1 = new BehaviorSubject<number>(0);
|
||||
subject2 = new BehaviorSubject<number>(0);
|
||||
subject3 = new BehaviorSubject<number>(0);
|
||||
subject4 = new BehaviorSubject<number>(0);
|
||||
subject5 = new BehaviorSubject<number>(0);
|
||||
subject6 = new BehaviorSubject<number>(0);
|
||||
});
|
||||
|
||||
function incrementAll() {
|
||||
subject1.next(subject1.getValue() + 1);
|
||||
subject2.next(subject2.getValue() + 1);
|
||||
subject3.next(subject3.getValue() + 1);
|
||||
subject4.next(subject4.getValue() + 1);
|
||||
subject5.next(subject5.getValue() + 1);
|
||||
subject6.next(subject6.getValue() + 1);
|
||||
}
|
||||
|
||||
test('should render once when all state changes are in click handler (react batch)', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
const value1 = useStateFromPublishingSubject(subject1);
|
||||
const value2 = useStateFromPublishingSubject(subject2);
|
||||
const value3 = useStateFromPublishingSubject(subject3);
|
||||
const value4 = useStateFromPublishingSubject(subject4);
|
||||
const value5 = useStateFromPublishingSubject(subject5);
|
||||
const value6 = useStateFromPublishingSubject(subject6);
|
||||
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button onClick={incrementAll} />
|
||||
<span>{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 0, value2: 0, value3: 0, value4: 0, value5: 0, value6: 0')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('2');
|
||||
});
|
||||
|
||||
test('should batch state updates when using useBatchedPublishingSubjects', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
const [value1, value2, value3, value4, value5, value6] = useBatchedPublishingSubjects(
|
||||
subject1,
|
||||
subject2,
|
||||
subject3,
|
||||
subject4,
|
||||
subject5,
|
||||
subject6
|
||||
);
|
||||
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
// using setTimeout to move next calls outside of callstack from onClick
|
||||
setTimeout(incrementAll, 0);
|
||||
}}
|
||||
/>
|
||||
<span>{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 0, value2: 0, value3: 0, value4: 0, value5: 0, value6: 0')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('2');
|
||||
});
|
||||
|
||||
test('should render for each state update outside of click handler', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
const value1 = useStateFromPublishingSubject(subject1);
|
||||
const value2 = useStateFromPublishingSubject(subject2);
|
||||
const value3 = useStateFromPublishingSubject(subject3);
|
||||
const value4 = useStateFromPublishingSubject(subject4);
|
||||
const value5 = useStateFromPublishingSubject(subject5);
|
||||
const value6 = useStateFromPublishingSubject(subject6);
|
||||
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
// using setTimeout to move next calls outside of callstack from onClick
|
||||
setTimeout(incrementAll, 0);
|
||||
}}
|
||||
/>
|
||||
<span>{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 0, value2: 0, value3: 0, value4: 0, value5: 0, value6: 0')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('7');
|
||||
});
|
||||
});
|
||||
|
||||
function incrementAll() {
|
||||
subject1.next(subject1.getValue() + 1);
|
||||
subject2.next(subject2.getValue() + 1);
|
||||
subject3.next(subject3.getValue() + 1);
|
||||
subject4.next(subject4.getValue() + 1);
|
||||
subject5.next(subject5.getValue() + 1);
|
||||
subject6.next(subject6.getValue() + 1);
|
||||
}
|
||||
describe('Publishing subject is undefined on first render', () => {
|
||||
test('useBatchedPublishingSubjects state should update when publishing subject is provided', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
// When subjects is expected to change, subjects must be part of react state.
|
||||
const [subjectFoo, setSubjectFoo] = useState<PublishingSubject<string> | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [valueFoo] = useBatchedPublishingSubjects(subjectFoo);
|
||||
|
||||
test('should render once when all state changes are in click handler (react batch)', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
const value1 = useStateFromPublishingSubject(subject1);
|
||||
const value2 = useStateFromPublishingSubject(subject2);
|
||||
const value3 = useStateFromPublishingSubject(subject3);
|
||||
const value4 = useStateFromPublishingSubject(subject4);
|
||||
const value5 = useStateFromPublishingSubject(subject5);
|
||||
const value6 = useStateFromPublishingSubject(subject6);
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
// using setTimeout to move next calls outside of callstack from onClick
|
||||
setTimeout(() => {
|
||||
setSubjectFoo(new BehaviorSubject<string>('foo'));
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
<span>{`valueFoo: ${valueFoo}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('valueFoo: undefined')).toBeInTheDocument();
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('valueFoo: foo')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('3');
|
||||
});
|
||||
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button onClick={incrementAll} />
|
||||
<span>{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 0, value2: 0, value3: 0, value4: 0, value5: 0, value6: 0')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('2');
|
||||
});
|
||||
test('useStateFromPublishingSubject state should update when publishing subject is provided', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
// When subject is expected to change, subject must be part of react state.
|
||||
const [subjectFoo, setSubjectFoo] = useState<PublishingSubject<string> | undefined>(
|
||||
undefined
|
||||
);
|
||||
const valueFoo = useStateFromPublishingSubject(subjectFoo);
|
||||
|
||||
test('should batch state updates when using useBatchedPublishingSubjects', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
const [value1, value2, value3, value4, value5, value6] = useBatchedPublishingSubjects(
|
||||
subject1,
|
||||
subject2,
|
||||
subject3,
|
||||
subject4,
|
||||
subject5,
|
||||
subject6
|
||||
);
|
||||
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
// using setTimeout to move next calls outside of callstack from onClick
|
||||
setTimeout(incrementAll, 0);
|
||||
}}
|
||||
/>
|
||||
<span>{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 0, value2: 0, value3: 0, value4: 0, value5: 0, value6: 0')
|
||||
).toBeInTheDocument();
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
// using setTimeout to move next calls outside of callstack from onClick
|
||||
setTimeout(() => {
|
||||
setSubjectFoo(new BehaviorSubject<string>('foo'));
|
||||
}, 0);
|
||||
}}
|
||||
/>
|
||||
<span>{`valueFoo: ${valueFoo}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('valueFoo: undefined')).toBeInTheDocument();
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('valueFoo: foo')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('3');
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('3');
|
||||
});
|
||||
|
||||
test('should render for each state update outside of click handler', async () => {
|
||||
let renderCount = 0;
|
||||
function Component() {
|
||||
const value1 = useStateFromPublishingSubject(subject1);
|
||||
const value2 = useStateFromPublishingSubject(subject2);
|
||||
const value3 = useStateFromPublishingSubject(subject3);
|
||||
const value4 = useStateFromPublishingSubject(subject4);
|
||||
const value5 = useStateFromPublishingSubject(subject5);
|
||||
const value6 = useStateFromPublishingSubject(subject6);
|
||||
|
||||
renderCount++;
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
// using setTimeout to move next calls outside of callstack from onClick
|
||||
setTimeout(incrementAll, 0);
|
||||
}}
|
||||
/>
|
||||
<span>{`value1: ${value1}, value2: ${value2}, value3: ${value3}, value4: ${value4}, value5: ${value5}, value6: ${value6}`}</span>
|
||||
<div data-test-subj="renderCount">{renderCount}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Component />);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 0, value2: 0, value3: 0, value4: 0, value5: 0, value6: 0')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('value1: 1, value2: 1, value3: 1, value4: 1, value5: 1, value6: 1')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('renderCount')).toHaveTextContent('7');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BehaviorSubject, skip } from 'rxjs';
|
||||
import { PublishingSubject, ValueFromPublishingSubject } from './types';
|
||||
|
||||
/**
|
||||
|
@ -30,16 +30,25 @@ export const usePublishingSubject = <T extends unknown = unknown>(
|
|||
/**
|
||||
* Declares a state variable that is synced with a publishing subject value.
|
||||
* @param subject Publishing subject.
|
||||
* When 'subject' is expected to change, 'subject' must be part of component react state.
|
||||
*/
|
||||
export const useStateFromPublishingSubject = <
|
||||
SubjectType extends PublishingSubject<any> | undefined = PublishingSubject<any> | undefined
|
||||
>(
|
||||
subject: SubjectType
|
||||
): ValueFromPublishingSubject<SubjectType> => {
|
||||
const isFirstRender = useRef(true);
|
||||
const [value, setValue] = useState<ValueFromPublishingSubject<SubjectType>>(subject?.getValue());
|
||||
useEffect(() => {
|
||||
if (!isFirstRender.current) {
|
||||
setValue(subject?.getValue());
|
||||
} else {
|
||||
isFirstRender.current = false;
|
||||
}
|
||||
|
||||
if (!subject) return;
|
||||
const subscription = subject.subscribe((newValue) => setValue(newValue));
|
||||
// When a new observer subscribes to a BehaviorSubject, it immediately receives the current value. Skip this emit.
|
||||
const subscription = subject.pipe(skip(1)).subscribe((newValue) => setValue(newValue));
|
||||
return () => subscription.unsubscribe();
|
||||
}, [subject]);
|
||||
return value;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue