Hi,
In this post I'll be showing some small React code samples and updating them to look a bit nicer (IMO) by focusing on React composition. The samples are very similar to what I used for actual production features and does not require extra libraries apart from React.
AB testing
A/B testing is a way to compare two versions of something and through analytics figuring out which version performs better.
Let's say we need to show one version (variant A) of CTA (call to action) button to 50% of users who land on a page and 2nd version (variant B) to other 50%. This is being done so we can measure conversion rate in the app and determine which copy attracts more clicks on the CTA. The variant "a" or "b" is deterministically calculated based on user's data which is not relevant for the example. This means every time a user lands on the page, he will always see the same variant (same button in this case):
export const CTAButton = () => {
const { variant } = useABExperiment('cta_button');
if (variant === 'a') {
return <Button>Learn more</Button>
}
{/* variant b testing the new button message */}
return <Button>Schedule appointment today</Button>;
};
Not too shabby.
Let's try a bit different approach:
export const CTAButton = () => (
<ABExperiment name='cta_button'>
<Variant name='a'>
<Button>Learn more</Button>
</Variant>
<Variant name='b'>
<Button>Schedule your online appointment today</Button>
</Variant>
</ABExperiment>
);
It is not important to focus on the implementation of <ABExperiment>
but more on the component composition. Focus on how a reviewer would feel to stumble upon this code during review vs a mixture of if
, else
, switch
for each AB test in code.
By Removing if/else branching, these new components are used to declaratively define the AB test.
Having the useABExperiment
hook, implementation would be straightforward:
<ABExperiment>
is a context that wraps the useABExperiment
hook that determines the variant 'a' or 'b' for that experiment based on user's data. <Variant>
uses the <ABExperiment>
context state and its name to determine whether to render or not. Bonus points if experiment and variant names are statically typed with TS.
After having 10 parallel AB experiments in a project each with its own if/else, if/return, switch... this seemed way more tidy.
Feature flags
Similar to the <ABExperiment />
API, it can be expanded to other places that use branching, like the feature flags:
const Plans = () => (
<ul>
<li><Button>Free</Button></li>
<li><Button>Basic</Button></li>
<li><Button>Premium</Button></li>
<FeatureFlag name='premium-ultra'>
<Flag name='on'>
<li><Button>Premium Ultra</Button></li>
</FlagOn>
<Flag name='off'>
<li><Button>Premium Plus</Button></li>
</FlagOff>
</FeatureFlag>
</ul>
)
Similar to ABExperiment, feature flags are remotely controlled and can be used to switch on/off features like the "premium-ultra plan feature" in this case.
Loading state
Loading state on the client side:
const SendMoneyForm = () => {
// loading remote data with react-query (NOT RELEVANT)
const { data, isLoading } = useCurrentUser()
const [amount, setAmount] = React.useState(0)
if (isLoading) return (
<>
<Skeleton size='md' />
<Button disabled>Loding...</Button>
</>
)
return (
<>
<CurrencySelector currency={data.currencies} />
<AmountInput amount={amount} onChange={setAmount} />
<Button>Send</Button>
</>
)
};
Switching it up with LoadingGuard
const SendMoneyForm = () => {
// loading remote data with react-query (NOT RELEVANT)
const { data, isLoading } = useCurrentUser()
const [amount, setAmount] = React.useState(0)
return (
<LoadingGuard
isLoading={isLoading}
skeleton={
<>
<Skeleton size='md' />
<Button disabled>Loding...</Button>
</>
}>
<CurrencySelector currencies={data.currencies} />
<AmountInput amount={amount} onChange={setAmount} />
<Button disabled={isLoading}>Send</Button>
</LoadingGuard>
)
Example implementation of LoadingGuard:
type Props = {
children: React.ReactNode;
isLoading?: boolean;
skeleton?: React.ReactNode;
};
export const LoadingGuard = ({
children,
isLoading,
skeleton
}: Props) => {
if (isLoading) return <>{skeleton}</>;
return <>{children}</>;
};
Platform switch
Let's say your product shares UX/UI (design-system components and/or front-end logic) between chrome extension and web app.
export const Settings = () => {
const platform = usePlatform();
return (
<ul>
<li><Button>Account settings</Button></li>
<li><Button>Security settings</Button></li>
<li><Button>Notification settings</Button></li>
{/* show this option in chrome-extension only, not on web */}
{platform === 'extension' && (
<li><Button>Sync data</Button></li>
)}
</ul>
);
};
Platform
component:
export const Settings = () => (
<ul>
<li><Button>Account settings</Button></li>
<li><Button>Security settings</Button></li>
<li><Button>Notification settings</Button></li>
{/* show this option in chrome-extension, not on web */}
<Platform name='extension'>
<li><Button>Sync data</Button></li>
<Platform />
</ul>
)
Inspiration
- React-router:
<Router>
<Route path='/x'>Yup</Route>
<Route path='/y'>basically</Route>
<Route path='/z'>same thing</Route>
</Router>
- Pattern matching in functional languages. Composition does not have anything to do with pattern matching, it's just that both deal with code branching in this case.
Final words
The ABExperiment
, FeatureFlag
, Platform
components remind me of internal-domain-specific-languages.
Internal DSLs are particular ways of using a host language to give the host language the feel of a particular language.
I think using components like these makes code easier to review, because verifying branching logic in PRs can get very tedious.
Could you think of another branching component similar to those mentioned?
Let me know whether your project could benefit by using components in this matter.
Top comments (0)