Reducing screen complexity with composing wrappers

lenilsondc

Lenilson de Castro

Posted on December 28, 2022

Reducing screen complexity with composing wrappers

Lately I've bem exploring new ways to organize screens in react native and this is a simple pattern to compose screens with wrappers.

The motivation for this pattern is to remove complexity from the working component and delegate the state management of components like toast, modal, loading overlay to something outside the working component.

The typical scenario where this pattern start making sense is when we have an ui component that is simply being added and managed in the working component and it's state has to be managed in every screen.

Simplified version of the problem:

function ExampleScreen(){
  const [toastVisible, setToastVisible] = useState(false);
  return (
    <>
      <Toast title="This is a toast" visible={toastVisible} />

      <Button
       title="Show toast"
       onClick={() => setToastVisible(true)}
      />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The problem with this approach IMO is that it doesn't scale well, every screen you have to repeat the state, binds and managing of that component's state. And as the number of components like this increases, it gets worse, now we have multiple of those thrown all over the screen definition.

const [toastVisible, setToastVisible] = useState(false);
Enter fullscreen mode Exit fullscreen mode

The solution I came up with is to replace all that with a hook that manages the state only once through a never changing context. Something like this:

function ExampleView(){
  const { showToast } = useToast();
  return (
    <>
      <Button
       title="Show toast"
       onClick={() => showToast("This is a toast")}
      />
    </>
  );
}

const ExampleScreen = () => (
  <ToastProvider>
    <ExampleView/>
  <ToastProvider/>
);
Enter fullscreen mode Exit fullscreen mode

I guess the philosophy of using such pattern is: it's not important how the toast is being handled, the screen concern is only to show the toast at a given moment. Usually a screen have a lot to manage on its own, so the idea is to limit the complexity to screen related things.

Pros:

  • Reduce usage complexity
  • Easy to use apis
  • Easier to reuse code

Cons:

  • Hide things from the working component
  • Performance might be a concern because of context api
  • Not all components can be used this way

Collaterals: The boilerplate

As every decision in software engineering, it comes with a cost, in this case, it increases the boilerplate. Hopefully, there are ways to abstract that away from the screen.

The problem is when we have multiple of those components being used, in this case, this solution is not an improvement at all, it is as much ugly as the previous one.

function ExampleView(){
  const { showToast } = useToast();
  const { showModal, hideModal } = useModal();
  const { showLoadingOverlay } = useLoadingOverlay();
  return (
    <>
      <Button
       title="Show toast"
       onClick={() => showToast("This is a toast")}
      />
      <Button
       title="Show modal"
       onClick={() => showToast({
        title: "Modal title",
        mainButton: {
          label: "Ok, got it!",
          onPress: hideModal,
        }
       })}
      />
      <Button
       title="Show loading"
       onClick={() => showLoadingOverlay()}
      />
    </>
  );
}

const ExampleScreen = () => (
  <LoadingOverlayProvider>
    <ModalProvider>
      <ToastProvider>
        <ExampleView/>
      <ToastProvider/>
    <ModalProvider/>
  <LoadingOverlayProvider/>
);
Enter fullscreen mode Exit fullscreen mode

For that, we have a know solution, we can use an HOC (higher order component) with a compose utility to reduce some of that complexity. At first, let's create a HOC to wrap a component with the context provider.

import React, { ComponentType } from 'react';

export function withWrapper<
  TComponentProps extends JSX.IntrinsicAttributes
>(Wrapper: ComponentType) {
  return (Component: ComponentType<TComponentProps>) =>
    (props: TComponentProps) =>
      (
        <Wrapper>
          <Component {...props} />
        </Wrapper>
      );
}
Enter fullscreen mode Exit fullscreen mode

Now things can be done like this:

const ExampleScreen = () => withWrapper(
  ToastProvider
)(ExampleView)
Enter fullscreen mode Exit fullscreen mode

Or for multiple providers:

const ExampleScreen = () => withWrapper(ToastProvider)(
  withWrapper(ModalProvider)(
    withWrapper(LoadingOverlayProvider)(ExampleView)
  )
)
Enter fullscreen mode Exit fullscreen mode

As we can see, applying the HOC on multiple wrappers still is a bit messy. Fortunately, we can borrow the compose concept from functional programing to apply those function in a more elegant way.

const ExampleScreen = () => compose(
  withWrapper(ToastProvider),
  withWrapper(ModalProvider),
  withWrapper(LoadingOverlayProvider),
)(ExampleView)
Enter fullscreen mode Exit fullscreen mode

We can even make named HOCs for each component to make it even more readable, so the finale version would look like this:

const ExampleScreen = () => compose(
  withToast(),
  withModal(),
  withLoadingOverlay(),
)(ExampleView);
Enter fullscreen mode Exit fullscreen mode

In addition, the HOC can receive props to configure the inner component if necessary:

withToast({ autoHide: true })
Enter fullscreen mode Exit fullscreen mode

Full working example can be found a this gist: https://gist.github.com/lenilsondc/48d82c84d050e610bf94cd1c067cefa9

Finally, this is not an optimal solution for many cases and reasons, it's very experimental, and even uses the infamous HOCs, which has been losing popularity lately. Nevertheless, it's a pretty clean way of building react screens. In the end it's all part of my journey of exploring functional programing applied to react seeking to make code look elegant.

I hope you've had a good read. Cheers!

💖 💪 🙅 🚩
lenilsondc
Lenilson de Castro

Posted on December 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related