Skip to content

Commit

Permalink
feat: memoize ToastManager context to avoid unnecessary re-renders
Browse files Browse the repository at this point in the history
  • Loading branch information
cheton committed Oct 13, 2023
1 parent 61fbf81 commit 2aff9cf
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Box,
Button,
Text,
Toast,
useToastManager,
} from '@tonic-ui/react';
import React from 'react';

const App = () => {
const toast = useToastManager();
const handleClickOpenToast = () => {
const render = ({ onClose, placement }) => {
const styleProps = {
'top-left': { pt: '2x', px: '4x' },
'top': { pt: '2x', px: '4x' },
'top-right': { pt: '2x', px: '4x' },
'bottom-left': { pb: '2x', px: '4x' },
'bottom': { pb: '2x', px: '4x' },
'bottom-right': { pb: '2x', px: '4x' },
}[placement];

return (
<Box
{...styleProps}
width={320}
>
<Toast isClosable onClose={onClose}>
<Text>This is a toast notification</Text>
</Toast>
</Box>
);
};
const options = {
placement: 'bottom-right',
duration: 5000,
};
toast(render, options);
};

return (
<Button onClick={handleClickOpenToast}>
Open Toast
</Button>
);
};

export default App;
Original file line number Diff line number Diff line change
Expand Up @@ -212,42 +212,4 @@ The toast state is a placement object, each placement contains an array of objec

## Demos

```jsx
function Example() {
const toast = useToastManager();
const handleClickOpenToast = () => {
const render = ({ onClose, placement }) => {
const styleProps = {
'top-left': { pt: '2x', px: '4x' },
'top': { pt: '2x', px: '4x' },
'top-right': { pt: '2x', px: '4x' },
'bottom-left': { pb: '2x', px: '4x' },
'bottom': { pb: '2x', px: '4x' },
'bottom-right': { pb: '2x', px: '4x' },
}[placement];

return (
<Box
{...styleProps}
width={320}
>
<Toast isClosable onClose={onClose}>
<Text>This is a toast notification</Text>
</Toast>
</Box>
);
};
const options = {
placement: 'bottom-right',
duration: 5000,
};
toast(render, options);
};

return (
<Button onClick={handleClickOpenToast}>
Open Toast
</Button>
);
}
```
{render('./useToastManager')}
49 changes: 25 additions & 24 deletions packages/react/src/toast/ToastManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useHydrated, useOnceWhen } from '@tonic-ui/react-hooks';
import { runIfFn, warnDeprecatedProps } from '@tonic-ui/utils';
import { ensureArray, ensureString } from 'ensure-type';
import memoize from 'micro-memoize';
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { isElement, isValidElementType } from 'react-is';
import {
TransitionGroup,
Expand Down Expand Up @@ -70,7 +70,7 @@ const ToastManager = ({
/**
* Create properties for a new toast
*/
const createToast = (message, options) => {
const createToast = useCallback((message, options) => {
const id = options?.id ?? uniqueId();
const duration = options?.duration;
const placement = ensureString(options?.placement ?? placementProp);
Expand All @@ -92,19 +92,21 @@ const ToastManager = ({
// The function to close the toast
onClose,
};
};
}, [close, placementProp]);

/**
* Close a toast record at its placement
*/
const close = (id, placement) => {
placement = placement ?? getToastPlacementByState(state, id);
const close = useCallback((id, placement) => {
setState((prevState) => {
placement = placement ?? getToastPlacementByState(prevState, id);

setState((prevState) => ({
...prevState,
[placement]: ensureArray(prevState[placement]).filter((toast) => toast.id !== id),
}));
};
return {
...prevState,
[placement]: ensureArray(prevState[placement]).filter((toast) => toast.id !== id),
};
});
}, []);

/**
* Close all toasts at once with the given placements, including the following:
Expand All @@ -115,12 +117,11 @@ const ToastManager = ({
* • bottom-left
* • bottom-right
*/
const closeAll = (options) => {
const placements = options?.placements
? ensureArray(options?.placements)
: Object.keys(state);

const closeAll = useCallback((options) => {
setState((prevState) => {
const placements = options?.placements
? ensureArray(options?.placements)
: Object.keys(prevState);
const nextState = placements.reduce((acc, placement) => {
acc[placement] = [];
return acc;
Expand All @@ -131,29 +132,29 @@ const ToastManager = ({
...nextState,
};
});
};
}, []);

/**
* Find the first toast in the array that matches the provided id. Otherwise, `undefined` is returned if not found.
* If no values satisfy the testing function, undefined is returned.
*/
const find = (id) => {
const find = useCallback((id) => {
const placement = getToastPlacementByState(state, id);
return ensureArray(state[placement]).find((toast) => toast.id === id);
};
}, [state]);

/**
* Find the first toast in the array that matches the provided id. Otherwise, -1 is returned if not found.
*/
const findIndex = (id) => {
const findIndex = useCallback((id) => {
const placement = getToastPlacementByState(state, id);
return ensureArray(state[placement]).findIndex((toast) => toast.id === id);
};
}, [state]);

/**
* Create a toast at the specified placement and return the id
*/
const notify = (message, options) => {
const notify = useCallback((message, options) => {
const { id, duration, placement } = options;
const toast = createToast(message, { id, duration, placement });

Expand Down Expand Up @@ -196,12 +197,12 @@ const ToastManager = ({
});

return toast.id;
};
}, [createToast]);

/**
* Update a specific toast with new options based on the given id. Returns `true` if the toast exists, else `false`.
*/
const update = (id, options) => {
const update = useCallback((id, options) => {
const placement = find(id)?.placement;
const index = findIndex(id);

Expand All @@ -219,7 +220,7 @@ const ToastManager = ({
});

return true;
};
}, [find, findIndex]);

const context = getMemoizedState({
// Methods
Expand Down
20 changes: 13 additions & 7 deletions packages/react/src/toast/useToastManager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useContext, useMemo } from 'react';
import { useContext, useRef } from 'react';
import { ToastManagerContext } from './context';

const useToastManager = () => {
const toastManagerRef = useRef(null);
const notifyRef = useRef(null);

if (!useContext) {
throw new Error('The `useContext` hook is not available with your React version.');
}
Expand All @@ -12,14 +15,17 @@ const useToastManager = () => {
throw new Error('The `useToastManager` hook must be called from a descendent of the `ToastManager`.');
}

const toast = useMemo(() => {
const fn = function (...args) {
return context.notify(...args);
notifyRef.current = context.notify;

if (!toastManagerRef.current) {
toastManagerRef.current = function (...args) {
return notifyRef.current?.(...args);
};
return Object.assign(fn, context);
}, [context]);
}

toastManagerRef.current = Object.assign(toastManagerRef.current, context);

return toast;
return toastManagerRef.current;
};

export default useToastManager;

0 comments on commit 2aff9cf

Please sign in to comment.