Skip to content

Commit

Permalink
Fixed close function dependency (#71)
Browse files Browse the repository at this point in the history
* Fixed close function dependency

* Refactoring
  • Loading branch information
AlexShukel authored Oct 7, 2022
1 parent b8741e0 commit 3e451c0
Show file tree
Hide file tree
Showing 20 changed files with 142 additions and 98 deletions.
3 changes: 2 additions & 1 deletion examples/WebpackApp/src/Alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ const AlertPopup: React.FunctionComponent<AlertPopupProps> = ({
</SnackbarPopupTemplate>
);

const props = {};
export const useAlert = () => {
const enqueueAlert = usePopupsFactory(AlertPopup, {}, SnackbarGroup);
const enqueueAlert = usePopupsFactory(AlertPopup, props, SnackbarGroup);

const showAlert = useCallback(
(message: string, severity: AlertColor) => {
Expand Down
11 changes: 8 additions & 3 deletions examples/WebpackApp/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import React, { useCallback } from 'react';
import { usePopup, useResponsePopup } from 'reactive-popups';

import { AlertTrigger } from './Alert';
import { ConfirmPopup } from './ConfirmPopup';
import { FalsyResponsePopup } from './FalsyResponsePopup';
import { ConfirmPopup, ConfirmPopupProps } from './ConfirmPopup';
import { MuiPopup } from './MuiPopup';
import { TestComponent } from './TestComponent';
import { DefaultPopupGroup } from '.';

export const App = () => {
const confirm = useResponsePopup(ConfirmPopup, {}, DefaultPopupGroup);
const confirm = useResponsePopup<ConfirmPopupProps, never, boolean>(
ConfirmPopup,
{},
DefaultPopupGroup
);
const [open, close] = usePopup(
MuiPopup,
{ content: 'Single popup example using usePopup hook' },
Expand Down Expand Up @@ -40,6 +44,7 @@ export const App = () => {
</button>
{/* <FalsyResponsePopup /> */}
<AlertTrigger />
<TestComponent />
</div>
);
};
2 changes: 1 addition & 1 deletion examples/WebpackApp/src/ConfirmPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const ConfirmPopup = ({ message }: ConfirmPopupProps) => {
setOpen(false);
}, []);

const { reject, resolve, unmount } = useResponseHandler(close);
const { reject, resolve, unmount } = useResponseHandler<boolean>(close);

return (
<Dialog
Expand Down
13 changes: 13 additions & 0 deletions examples/WebpackApp/src/TestComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React, { useEffect } from 'react';

import { useAlert } from './Alert';

export const TestComponent = () => {
const show = useAlert();

useEffect(() => {
show('Show this message on mount!', 'success');
}, [show]);

return <div>test</div>;
};
7 changes: 0 additions & 7 deletions src/constants.ts

This file was deleted.

9 changes: 4 additions & 5 deletions src/hooks/useCloseHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useCallback, useEffect } from 'react';

import { usePopupsContext } from './usePopupsContext';
import { CLOSE_HANDLER_BAD_USE } from '../constants';
import { isDefaultPopup } from '../types/DefaultPopup';
import { usePopupIdentifier } from '../utils/PopupIdentifierContext';

Expand All @@ -20,15 +19,15 @@ export const useCloseHandler = (
const popup = getPopup(popupIdentifier);

if (!isDefaultPopup(popup!)) {
throw new Error(CLOSE_HANDLER_BAD_USE);
throw new Error(
'useCloseHandler hook must be used only in popups created with usePopupsFactory.'
);
}

if (close) {
popup.setCloseHandler(close);
}
// BUG with throwing error if getPopup is in dependency list
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [popupIdentifier, close]);
}, [popupIdentifier, close, getPopup]);

return unmountPopup;
};
11 changes: 8 additions & 3 deletions src/hooks/usePopup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const usePopup = <P, K extends keyof P>(
props: Pick<P, K>,
group: PopupGroup
): UsePopupBag<P, K> => {
const { mount, close: closePopup } = usePopupsContext();
const { mount, close: closePopup, unmount } = usePopupsContext();

const popupIdentifier = useRef<PopupIdentifier>({
id: uuid(),
Expand All @@ -26,15 +26,20 @@ export const usePopup = <P, K extends keyof P>(

const open = useCallback<OptionalParamFunction<Omit<P, K>, void>>(
(omittedProps?: Omit<P, K>) => {
const defaultClose = () => {
unmount(popupIdentifier.current);
};

const popup = new DefaultPopup(
PopupComponent as ComponentType<{}>,
{ ...props, ...omittedProps },
popupIdentifier.current
popupIdentifier.current,
defaultClose
);

mount(popup);
},
[PopupComponent, mount, props]
[PopupComponent, mount, props, unmount]
);

const close = useCallback(() => {
Expand Down
41 changes: 14 additions & 27 deletions src/hooks/usePopupsBag.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,33 @@
import { useCallback, useReducer } from 'react';
import { useCallback, useReducer, useRef } from 'react';

import { PopupGroup } from '../components/PopupGroup';
import { Popup } from '../types/Popup';
import { PopupIdentifier } from '../types/PopupIdentifier';
import { PopupsBag } from '../types/PopupsBag';
import { popupsReducer } from '../utils/popupsReducer';
import { ActionType, popupsReducer } from '../utils/popupsReducer';

export const usePopupsBag = (): PopupsBag => {
const [popupsState, dispatch] = useReducer(popupsReducer, { popups: {} });

const getPopupsByGroup = useCallback(
(group: PopupGroup) => {
if (!popupsState.popups[group.groupId]) {
return [];
}
const popupsStateRef = useRef(popupsState);
popupsStateRef.current = popupsState;

return Object.values(popupsState.popups[group.groupId]);
},
[popupsState]
);
const getPopup = useCallback(({ groupId, id }: PopupIdentifier) => {
const popups = popupsStateRef.current.popups;

const getPopup = useCallback(
({ groupId, id }: PopupIdentifier) => {
if (
!popupsState.popups[groupId] ||
!popupsState.popups[groupId][id]
) {
return null;
}
if (!popups[groupId] || !popups[groupId][id]) {
return null;
}

return popupsState.popups[groupId][id];
},
[popupsState]
);
return popups[groupId][id];
}, []);

const unmount = useCallback((popupIdentifier: PopupIdentifier) => {
dispatch({ type: 'unmount', payload: { popupIdentifier } });
dispatch({ type: ActionType.UNMOUNT, payload: { popupIdentifier } });
}, []);

const mount = useCallback(<P = {}>(popup: Popup<P>) => {
dispatch({
type: 'mount',
type: ActionType.MOUNT,
payload: {
popup: popup as unknown as Popup<object>,
},
Expand All @@ -60,8 +47,8 @@ export const usePopupsBag = (): PopupsBag => {
return {
mount,
unmount,
getPopupsByGroup,
getPopup,
close,
popupsState,
};
};
8 changes: 5 additions & 3 deletions src/hooks/usePopupsByGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { PopupGroup } from '../components/PopupGroup';
import { Popup } from '../types/Popup';

export const usePopupsByGroup = (group: PopupGroup): Popup<object>[] => {
const { getPopupsByGroup } = usePopupsContext();
const { popupsState } = usePopupsContext();

const popups = getPopupsByGroup(group);
if (!popupsState.popups[group.groupId]) {
return [];
}

return popups;
return Object.values(popupsState.popups[group.groupId]);
};
24 changes: 16 additions & 8 deletions src/hooks/usePopupsFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { usePopupsContext } from './usePopupsContext';
import { PopupGroup } from '../components/PopupGroup';
import { DefaultPopup } from '../types/DefaultPopup';
import { OptionalParamFunction } from '../types/OptionalParamFunction';
import { PopupIdentifier } from '../types/PopupIdentifier';
import { uuid } from '../utils/uuid';

export type UsePopupsFactoryBag<P, K extends keyof P> = OptionalParamFunction<
Expand All @@ -16,31 +17,38 @@ export const usePopupsFactory = <P, K extends keyof P>(
props: Pick<P, K>,
group: PopupGroup
): UsePopupsFactoryBag<P, K> => {
const { mount, close } = usePopupsContext();
const { mount, unmount } = usePopupsContext();

const create = useCallback(
(omittedProps?: Omit<P, K>) => {
const id = uuid();

const popupIdentifier: PopupIdentifier = {
id,
groupId: group.groupId,
};

const defaultClose = () => {
unmount(popupIdentifier);
};

const popup = new DefaultPopup(
PopupComponent,
{
...props,
...omittedProps,
} as P,
{
id,
groupId: group.groupId,
}
popupIdentifier,
defaultClose
);

const identifier = mount<P>(popup);
mount<P>(popup);

return () => {
close(identifier);
unmount(popupIdentifier);
};
},
[mount, PopupComponent, props, group, close]
[group.groupId, PopupComponent, props, mount, unmount]
);

return create;
Expand Down
23 changes: 14 additions & 9 deletions src/hooks/useResponseHandler.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { useCallback, useEffect, useRef } from 'react';

import { usePopupsContext } from './usePopupsContext';
import { PROMISE_NOT_SETTLED, RESPONSE_HANDLER_BAD_USE } from '../constants';
import { isResponsePopup, ResponsePopup } from '../types/ResponsePopup';
import { usePopupIdentifier } from '../utils/PopupIdentifierContext';

export type ResponseHandler = {
resolve: (value: unknown | PromiseLike<unknown>) => void;
export type ResponseHandler<R> = {
resolve: (value: R | PromiseLike<R>) => void;
reject: (reason?: unknown) => void;
unmount: () => void;
};

export const useResponseHandler = (close: () => void): ResponseHandler => {
export const useResponseHandler = <R>(
close: () => void
): ResponseHandler<R> => {
const {
getPopup,
close: closePopup,
unmount: unmountPopup,
} = usePopupsContext();
const popupIdentifier = usePopupIdentifier();

const popupRef = useRef<ResponsePopup<object, unknown> | null>(null);
const popupRef = useRef<ResponsePopup<object, R> | null>(null);

const resolve = useCallback(
(value: unknown) => {
(value: R | PromiseLike<R>) => {
popupRef.current!.resolve!(value);
closePopup(popupIdentifier);
},
Expand All @@ -38,8 +39,10 @@ export const useResponseHandler = (close: () => void): ResponseHandler => {
);

const unmount = useCallback(() => {
if (!popupRef.current!.isSettled!) {
throw new Error(PROMISE_NOT_SETTLED);
if (!popupRef.current!.isSettled) {
throw new Error(
'Promise from ResponsePopup was not settled (memory leak).'
);
}

unmountPopup(popupIdentifier);
Expand All @@ -49,7 +52,9 @@ export const useResponseHandler = (close: () => void): ResponseHandler => {
const popup = getPopup(popupIdentifier);

if (!isResponsePopup(popup!)) {
throw new Error(RESPONSE_HANDLER_BAD_USE);
throw new Error(
'useResponseHandler hook must be used only in popups created with useResponsePopup.'
);
}

popup.setCloseHandler(close);
Expand Down
23 changes: 15 additions & 8 deletions src/hooks/useResponsePopup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ComponentType, useCallback } from 'react';
import { ComponentType, useCallback, useRef } from 'react';

import { usePopupsContext } from './usePopupsContext';
import { PopupGroup } from '../components/PopupGroup';
import { OptionalParamFunction } from '../types/OptionalParamFunction';
import { PopupIdentifier } from '../types/PopupIdentifier';
import { ResponsePopup } from '../types/ResponsePopup';
import { uuid } from '../utils/uuid';

Expand All @@ -17,11 +18,19 @@ export const useResponsePopup = <P, K extends keyof P, R>(
props: Pick<P, K>,
group: PopupGroup
): UseResponsePopupBag<P, K, R> => {
const { mount } = usePopupsContext();
const { mount, unmount } = usePopupsContext();

const popupIdentifierRef = useRef<PopupIdentifier>({
id: uuid(),
groupId: group.groupId,
});

const defaultClose = useCallback(() => {
unmount(popupIdentifierRef.current);
}, [unmount]);

const waitResponse = useCallback(
(omittedProps?: Omit<P, K>) => {
const id = uuid();
let popup: ResponsePopup<P, R> | null = null;

const promise = new Promise<R>((resolve, reject) => {
Expand All @@ -31,10 +40,8 @@ export const useResponsePopup = <P, K extends keyof P, R>(
...props,
...omittedProps,
} as P,
{
id,
groupId: group.groupId,
},
popupIdentifierRef.current,
defaultClose,
resolve,
reject
);
Expand All @@ -48,7 +55,7 @@ export const useResponsePopup = <P, K extends keyof P, R>(

return promise;
},
[PopupComponent, group, mount, props]
[PopupComponent, defaultClose, mount, props]
);

return waitResponse;
Expand Down
7 changes: 7 additions & 0 deletions src/hooks/useUnmount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { usePopupsContext } from './usePopupsContext';

export const useUnmount = () => {
const { unmount } = usePopupsContext();

return unmount;
};
Loading

0 comments on commit 3e451c0

Please sign in to comment.