In modern frontend development, a common goal is to create flexible and reusable components that can be configured for different use cases. A perfect example of this is designing a generic completion screen that can be tailored to various flows within an application. The key to achieving this lies in making the frontend “dumb,” meaning it doesn’t dictate the UI layout or behavior but instead responds to data and configurations provided by the backend.
The Concept of a “Dumb” Frontend
A “dumb” frontend isn’t unintelligent—it’s intentionally designed to be generic and adaptable. It doesn’t make assumptions about the content or layout. Instead, it dynamically renders UI components based on the data it receives. This approach is particularly powerful when you need to create different screens or elements within the same component framework.
By making the frontend dumb, you offload the responsibility of deciding the UI structure to the backend, which can deliver different configurations depending on the context. This way, the same frontend code can handle various scenarios, reducing duplication and making maintenance easier.
A Generic Completion Screen
Imagine you need to design a completion screen for different flows in your web application. Depending on the flow, the screen might display different combinations of images, animations, text, and call-to-action (CTA) buttons. Instead of creating a separate component for each variation, you can create a single, configurable component.
Here’s an example of what this might look like in code:
import { ImageOrAnimationBanner } from '@web-components'
import { Typography } from '../../../shared/src/ComponentsV2/common/TypographyV2'
import { DynamicContentWrapper } from './styles'
import { DynamicContentProps } from './types'
import Spacer from 'src/components/shared/src/Components/utils/Spacer'
import { useCallback } from 'react'
import { Button } from 'src/components/shared/src/dls/atomic/Button'
import useDynamicContent from './useDynamicContent'
const DynamicContent = (props: DynamicContentProps) => {
const { data } = props || {}
const { items = [] } = data || {}
const { handleCtaClick, isInProgress } = useDynamicContent(props)
const renderListingItem = useCallback((item, index) => {
const { config, type } = item
switch (type) {
case 'text': {
const { text, variant = 'body-base-regular', color } = config
return (
<div key={'text_' + index} style={{ color }}>
<Typography variant={variant}>
<span dangerouslySetInnerHTML={{ __html: text }} />
</Typography>
</div>
)
}
case 'spacer': {
const { height } = config
return <Spacer key={'spacer_' + index} height={height} />
}
case 'banner': {
const { bannerData } = config
return (
<ImageOrAnimationBanner key={'banner_' + index} {...bannerData} />
)
}
case 'cta': {
const { ctaText, buttonProps = {}, ctaActions = [] } = config
return (
<Button
variant="PRIMARY"
label={ctaText}
onClick={handleCtaClick(ctaActions)}
disabled={isInProgress}
{...buttonProps}
/>
)
}
}
return null
}, [])
return (
<DynamicContentWrapper>
{items.map(renderListingItem)}
</DynamicContentWrapper>
)
}
export default DynamicContent
Breaking Down the Code
DynamicContent Component: This component is designed to be as flexible as possible. It doesn’t know or care what kind of content it’s rendering—it simply iterates over an array of items, each with a specific type and configuration.
Rendering Different Content Types: The
renderListingItem
function handles different types of content like text, banners, and CTAs. For each type, it reads the configuration and renders the appropriate component.Backend-Driven Configuration: The backend provides an array of items, each with a
type
andconfig
. Thetype
determines which component to render, while theconfig
dictates the component’s appearance and behavior. For instance, the CTA button’s text, styling, and actions are all configurable.Spacer Component: To handle spacing dynamically, a
Spacer
component is used. The height of the spacer can be controlled via the backend, allowing for precise control over the layout without hardcoding values in the frontend.
Why Make the Frontend Dumb?
This approach has several advantages:
Flexibility: The same frontend component can handle a wide variety of use cases without any code changes.
Consistency: By centralizing the configuration in the backend, you ensure that all screens follow the same design language and behavior patterns.
Maintainability: Any updates to the UI or behavior can be made in the backend, reducing the need to modify frontend code.
However, a “dumb” frontend still needs to be “smart” in how it handles the data it receives. It must gracefully manage cases where the backend might send unexpected or incomplete configurations. It should also ensure that UI consistency is maintained by following the design language system (DLS) practices, such as using predefined sizes, colors, and typography.
Conclusion and Next Steps
In this blog, we explored how to design a configurable completion screen using a smart backend and a dumb frontend. This approach not only simplifies frontend development but also makes the UI highly adaptable and maintainable.
In the next blog, we'll dive into creating a custom hook that can handle different kinds of actions triggered by various UI elements, further extending the flexibility and power of this approach.
So, how would you design a completion screen with these principles in mind? Think about the configuration possibilities and come back tomorrow to see how to implement it.