Designing UI architecture for customizable user experiences
Whether you want to show users different components based on where in they are in the user journey (e.g. first time experience) or you want to shows certain components to specific segments of your user base, it’s easy for this business logic to become a jumbled mess. Here’s are a couple of practices for keeping things orderly:
Use arrays to keep logic legible
Referring to React here, I find arrays of logic and components to be much more legible than using `{ }` within a component’s return statement.
Consider the two code snippets (React + Typescript), asking yourself which one is more legible.
What I discourage:
const View = () => {
...
return (
<>
{account.email && &&
newsletters.length > 0 &&
account.confirmed &&
(
<div className="px-2 sm:px-0">
<ConfirmEmailNotice email={data.account?.email} />
</div>
)}
<div className="sticky top-0 z-20 bg-neutral-100 p-2">
<h1 className="text-3xl text-slate-900">Home</h1>
</div>
{newsletters.length === 0 && (
<div className="mt-16 flex flex-col items-center gap-y-12">
<h2 className="border-l-4 border-solid border-gray-500">
When your newsletters get summarized, they'll
show up here. Go subscribe to some.
</h2>
<Button className="w-full" href="/discover">
Discover Newsletters
<ArrowRightIcon className="ml-2 text-lg" />
</Button>
</div>
)}
...
</>
)
}
What I encourage:
const viewComponents: [string, (() => JSX.Element) | null][] = [
[
"confirm-email-notice",
account.email &&
newsletters.length > 0 &&
account?.confirmed
? () => (
<div className="px-2 sm:px-0">
<ConfirmEmailNotice email={data.account?.email} />
</div>
)
: null,
],
[
"header",
() => (
<div className="sticky top-0 z-20 bg-neutral-100 p-2">
<h1 className="text-3xl text-slate-900">Home</h1>
</div>
),
],
[
"zero-state-with-inbox",
data.newsletters.length === 0 && data.account.moondipInbox
? () => (
<div className="mt-16 flex flex-col items-center gap-y-12">
<h2 className="border-l-4 border-solid border-gray-500">
When your newsletters get summarized, they'll
show up here. Go subscribe to some.
</h2>
<Button className="w-full" href="/discover">
Discover Newsletters
<ArrowRightIcon className="ml-2 text-lg" />
</Button>
</div>
)
: null,
],
...
]
return (
...
{viewComponents.map(([id, renderComponent]) => renderComponent ? (
<React.Fragment key={id}>
{renderComponent()}
</React.Fragment>
) : null
)}
...
);
I find the second example to be much easier to read.
The logic and JSX components are visually more separate
Each item in the list has a descriptive string that also serves as a unique id, making it easy to figure out what each item renders while maintaining a flatter component architecture. Meanwhile, comments embedded between curly braces add noise.
Prevent breaking changes with integration tests
Regardless, this kind of logic in your application is bound to break if you continuously add to it. For the sake of ensuring that your users are getting the UI you expect, build integration tests that test that the expected components are rendering for every scenario you’re building for.