Reducing screen complexity with composing wrappers
Lenilson de Castro
Posted on December 28, 2022
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)}
/>
</>
);
}
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);
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/>
);
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/>
);
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>
);
}
Now things can be done like this:
const ExampleScreen = () => withWrapper(
ToastProvider
)(ExampleView)
Or for multiple providers:
const ExampleScreen = () => withWrapper(ToastProvider)(
withWrapper(ModalProvider)(
withWrapper(LoadingOverlayProvider)(ExampleView)
)
)
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)
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);
In addition, the HOC can receive props to configure the inner component if necessary:
withToast({ autoHide: true })
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!
Posted on December 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.