How the ‘overlay’ component works in NativeBase

How the ‘overlay’ component works in NativeBase

The overlay component provides contextual information and options in the form of an additional layer on top of the current state of the interface. They are either purposefully interruptive like modals or augmenting like popovers and tooltips.

This article explains how it works in detail.

Why use an overlay component?

Here is a quick example using the Alert Dialog box. In it, the NativeBase AlertDialog component confirms whether you want to delete a customer or not.

Such overlay components allow us to take quick information or confirmation from users. This is done without actual navigation or re-routing them to a new screen.

Here is the code snippet of the components.⤵️

const Example = () => {
  const [isOpen, setIsOpen] = React.useState(false);

  const onClose = () => setIsOpen(false);

  const cancelRef = React.useRef(null);
  return <Center>
      <Button shadow={2} colorScheme="danger" onPress={() => setIsOpen(!isOpen)}>
        Delete Customer
      </Button>
      <AlertDialog leastDestructiveRef={cancelRef} isOpen={isOpen} onClose={onClose}>
        <AlertDialog.Content>
          <AlertDialog.CloseButton />
          <AlertDialog.Header>Delete Customer</AlertDialog.Header>
          <AlertDialog.Body>
            This will remove all data relating to Alex. This action cannot be
            reversed. Deleted data can not be recovered.
          </AlertDialog.Body>
          <AlertDialog.Footer>
            <Button.Group space={2}>
              <Button variant="unstyled" colorScheme="coolGray" onPress={onClose} ref={cancelRef}>
                Cancel
              </Button>
              <Button colorScheme="danger" onPress={onClose}>
                Delete
              </Button>
            </Button.Group>
          </AlertDialog.Footer>
        </AlertDialog.Content>
      </AlertDialog>
    </Center>;
};

Here is a video example ⤵️ AlertDialogBox Example.gif

Overlay component using React.Portal in web

const Example = () => {
  <Box bg="red.400" w="200px" h="200px">
    <Box bg="yellow.400" w="100%" h="100%">
      <Modal /> {/* line 4 */}
      <Box bg="green.400" w="100%" h="100%" /> 
    </Box>
  </Box>;
};

When we write a Modal inside an element (Parent element line 4) there is a possibility that the modal will be cropped or covered up by other elements. To avoid this, we use CSS properties like overflow: hidden and z-index to ensure it appears on top.

Sometimes the modal is in a child that is nested a dozen level-deep. In this case, it is difficult to modify the style of the child without breaking the one from the parent. To avoid this, we want to take the modal out of this hierarchy. This will allow us to manipulate it without affecting the parents. For this, we have a Portal. It takes the child (any renderable React element or JSX) and container (a DOM element where the child gets rendered) as arguments.

Therefore, when we create a Portal for our Modal it will not render in the root div but instead modal-root. This concept is shown below.

<html>
  <body>
    <div id="root"></div>
    <div id="modal-root"> <Modal/> </div>
  </body>
</html>

More about React portal.

Overlay component in React-native

In React-native, we don't have a Portal. To achieve a similar effect, we use React context.

Here is an example:

const PortalContext = React.createContext<PortalContext | null>(null);

interface PortalContext {
  items: Array<OverlayItem>;
  setOverlayItem: (node: ReactNode) => number;
  removeOverlayItem: (id: number) => void;
  updateOverlayItem: (id: number, node: ReactNode) => void;
}

If we look at our context from react-native-aria for Portal, we need to understand four things:

  1. Items represent Array of OverlayItems
  2. setOverlayItem is a setter for our items
  3. removeOverlayItem deletes an item with a particular id
  4. updateOverlayItem updates an item with a particular id

Here's how we can implement this ⤵️

 function Portal(props: ModalProviderProps) {
  const context = usePortalProvider();
  const overlayId = React.useRef<number | undefined>(undefined);
  let contents = <OverlayView {...props} />;

  useEffect(
    () => {
      // Mount
      if (overlayId.current === undefined) {
        overlayId.current = context?.setOverlayItem(contents);
      }
      // Update
      else {
        if (overlayId.current) {
          context?.updateOverlayItem(overlayId.current, contents);
        }
      }
    },
    // To re-render the child
    [props]
  );

  // Unmount
  useEffect(() => {
    return () => {
      if (overlayId.current) {
        context?.removeOverlayItem(overlayId.current);
      }
    };
  }, []);

  return null;
}

We first grab our PortalContext and create a ref for our Portal. Then we create content which is a view with style={[StyleSheet.absoluteFill, style]}. We then create useEffect with props as a dependency which calls setOverlayItem to push our content to the item array and update it when props are changed.

To finish up, we add a cleanup function in another useEffect which calls removeOverlayItem with ID to delete Overlay from an item array. in the end, we return null which doesn't print anything where it's actually written.

PortalProvider

In Provider, we define our context values. We render children which is a root, and then as a sibling, we map over all the overlay items to render them one by one.

export function PortalProvider(props: { children: ReactNode }) {
  const [items, setItems] = React.useState<Array<OverlayItem>>([]);

  const setOverlayItem = (item: ReactNode) => {
    const overlayId = ++globalOverlayCounter;
    setItems((prev) => prev.concat([{ id: overlayId, node: item }]));
    return overlayId;
  };

  const updateOverlayItem = (id: number, node: ReactNode) => {
    setItems((prev) =>
      prev.map((item) => {
        if (item.id === id) {
          return { id, node };
        }
        return item;
      })
    );
  };

  const removeOverlayItem = (id: number) => {
    setItems((prev) => {
      const newItems = prev.filter((item) => item.id !== id);
      return newItems;
    });
  };

  return (
    <PortalContext.Provider
      value={{ items, setOverlayItem, removeOverlayItem, updateOverlayItem }}
    >
      {props.children}

      {/* Render Overlays */}
      {items.map((item) => {
        return <React.Fragment key={item.id}>{item.node}</React.Fragment>;
      })}
    </PortalContext.Provider>
  );
}

At the end inside NativeBaseProvider, we wrap children with PortalProvider.

 <NativeBaseConfigProvider
      theme={newTheme}
      config={config}
      currentBreakpoint={currentBreakpoint}
      isSSR={isSSR}
      disableContrastText={disableContrastText}
    >
      <SafeAreaProvider
        initialMetrics={
          initialWindowMetrics ?? defaultInitialWindowMetricsBasedOnPlatform
        }
      >
        <ResponsiveQueryProvider disableCSSMediaQueries={!isSSR}>
          <HybridProvider
            colorModeManager={colorModeManager}
            options={theme.config}
          >
            <PortalProvider>
              <ToastProvider>
                <InitializeToastRef />
                <SSRProvider>{children}</SSRProvider>
              </ToastProvider>
            </PortalProvider>
          </HybridProvider>
        </ResponsiveQueryProvider>
      </SafeAreaProvider>
    </NativeBaseConfigProvider>

Difficulties of using overlay in React-native

  • Accessibility: Since it's a view providing Modal or overlay components accessibility is difficult.
  • Prioritization: Maintaining priority for overlay components is difficult. Suppose we have toast as an overlay component which should be displayed on top even if Modal is rendered after toast. In this case, we have to insert toast such that it will always be at the end of the item array.
  • RN Modal: Since our overlay is View, RN Modal will always be on top and our overlay components will render behind.

Summary

Overlay components in react native can be achieved via React.Context but managing their Accessibility and making them feel like react portal is difficult. If we are okay with this trade-off. it’s great to have them in our application.