[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:
Nathan Reese 2024-03-27 14:14:42 -06:00 committed by GitHub
parent 176ea275a8
commit d54200bc42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 248 additions and 231 deletions

View file

@ -9,7 +9,6 @@
export {
apiCanDuplicatePanels,
apiCanExpandPanels,
useExpandedPanelId,
type CanDuplicatePanels,
type CanExpandPanels,
} from './interfaces/panel_management';

View file

@ -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);

View file

@ -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';

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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> = [

View file

@ -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');
});
});

View file

@ -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;