01 / The idea
Working out the banner and recording it together is a fair start.
You’re building the dashboard for a SaaS product with a free trial. trialBanner(account) works out how many days are left, picks a message, and
records a trial_banner_shown event so the growth team can see who saw it. One function,
one call when the page loads, and everything about the banner in one place.
Read the first bannerTypeScript · the version this lesson starts from
// The first version: work out the banner, and record that it was shown.
export function trialBanner(account: Account): Banner {
const today = new Date().toISOString().slice(0, 10); // Reads the clock.
const daysLeft = daysBetween(today, account.trialEndsOn);
track({ name: 'trial_banner_shown', accountId: account.id, daysLeft }); // Records an impression.
if (daysLeft < 0) return { daysLeft, tone: 'ended', message: 'Your trial has ended' };
if (daysLeft === 0) return { daysLeft, tone: 'urgent', message: 'Your trial ends today' };
const message = `${daysLeft} ${daysLeft === 1 ? 'day' : 'days'} left in your trial`;
return { daysLeft, tone: daysLeft <= 3 ? 'urgent' : 'info', message };
} Go’s version calls time.Now() and appends to a package-level event list the
same way. Both languages meet again at bannerOn in section 02.
Then the banner moves into a component. It’s worked out on every render, and the growth dashboard shows three impressions for each visit. The test for “Your trial ends today” passes in the afternoon and fails after midnight UTC. And a customer in California writes in at 6 pm to say the banner already counts tomorrow.
A pure function gets everything it depends on through its parameters, and does nothing but return a value. Reading the clock is an input, even when it isn’t a parameter; recording an event is an output, even when nothing is returned. Put the decision in a pure function and the reads and writes in a thin shell around it, and the decision can run any number of times, in any test, at any hour. React’s documentation puts the rule in four words: “Same inputs, same output.”
Section 05 builds a trial banner that updates at midnight and records one impression a day, in React and Svelte.
02 / See the shape
Pass in what it reads, and return what it decides.
The basic form is a pure function: the day is a parameter, and the banner is the only result. In the wild adds the shell that reads the clock and records the event, once each. At the call site works out four days of banners and shows one recorded event.
Both languages produce the same messages, tones, and event count.
A pure function. bannerOn takes the trial end date and the day as parameters, and only returns a banner.
// Everything the banner depends on is a parameter, and all it does is return a value.
export function bannerOn(trialEndsOn: string, today: string): Banner {
const daysLeft = daysBetween(today, trialEndsOn);
if (daysLeft < 0) return { daysLeft, tone: 'ended', message: 'Your trial has ended' };
if (daysLeft === 0) return { daysLeft, tone: 'urgent', message: 'Your trial ends today' };
const message = `${daysLeft} ${daysLeft === 1 ? 'day' : 'days'} left in your trial`;
return { daysLeft, tone: daysLeft <= 3 ? 'urgent' : 'info', message };
} // BannerOn takes everything the banner depends on as a parameter, and only returns a value.
func BannerOn(trialEndsOn, today string) Banner {
daysLeft := DaysBetween(today, trialEndsOn)
switch {
case daysLeft < 0:
return Banner{daysLeft, "ended", "Your trial has ended"}
case daysLeft == 0:
return Banner{daysLeft, "urgent", "Your trial ends today"}
}
unit, tone := "days", "info"
if daysLeft == 1 {
unit = "day"
}
if daysLeft <= 3 {
tone = "urgent"
}
return Banner{daysLeft, tone, fmt.Sprintf("%d %s left in your trial", daysLeft, unit)}
} Reading the TypeScriptHidden inputs and a small interface
The first version needs vi.useFakeTimers and vi.setSystemTime before its test means anything. bannerOn needs a date string. new Date().toISOString() is also why the customer in California
saw tomorrow: MDN notes that “the timezone is always UTC.”
Clock and Analytics are one-method interfaces, so a test passes
an object literal and counts what happened.
Reading the GoInterfaces at the edge
The first version’s hidden input is time.Now(), and its hidden output is
the package-level Sent slice. BannerOn takes two strings and
returns a Banner value.
The shell takes Clock and Analytics interfaces. The example
satisfies them with a fixedClock string type and a small recorder, the same way
a test would.
03 / Follow the calls
Watch what each call reads and records.
Five steps, each running the lesson’s functions. On the left are the calls and what they returned; on the right is everything they touched outside themselves. Before each step, guess how many events it records.
In Try it, pick a day and call each version, including the one that reads your real clock.
What did the function touch?
The same argument, two answers. trialBanner(account) at 23:59 UTC returns “4 days left in your trial”; trialBanner(account) at 00:01 UTC returns “3 days left in your trial”. Clock reads: 2. Events recorded: 2. Nothing about the argument changed. The function read the clock, which is an input you can’t see in the call.
Two minutes apart, two answers.
trialBanner(account) says 4 days left at 23:59 UTC and 3 days left at 00:01. It read the clock both times.
Reduced motion: choose a scene to see its completed state.
Read this scene
trialBanner(account) says 4 days left at 23:59 UTC and 3 days left at 00:01. It read the clock both times.
The same argument, two answers. trialBanner(account) at 23:59 UTC returns “4 days left in your trial”; trialBanner(account) at 00:01 UTC returns “3 days left in your trial”. Clock reads: 2. Events recorded: 2. Nothing about the argument changed. The function read the clock, which is an input you can’t see in the call.
Watch restarts when you return. Step through keeps your selected step. Try it starts with no calls each time you open it.
What a pure core buys you
Now put names on what you just watched. These are the words you’ll hear in a design review, and each one points at something on this page.
- Answers you can predict
bannerOn('2026-09-17', '2026-09-14')says 3 days left, whenever and however often it runs.- Tests without a fake clock
- The last day, the day after, and one day left are each a call with a date.
- Safe to run again
- Ten more calculations, as re-renders do, record nothing.
- Effects you can count
showTrialBannerreads the clock once and records one event.- Reuse without surprises
- A reminder email can call
bannerOnwithout recording impressions nobody saw.
The review words are pure function, side effect, hidden input for the clock the call doesn’t mention, referential transparency for being able to replace a call with its result, and functional core, imperative shell for the split. Section 08 covers what they cost.
04 / Try a decision
A cache that remembers yesterday.
bannerOn is pure, so someone cached it. The code is in cached.ts, and the lesson’s tests pin what happens.
05 / Give it a real job
A banner that’s right at midnight and counted once a day.
In the real dashboard, people leave the tab open for days. The banner has to change at local midnight without a reload, record one impression per day it’s seen, and let people dismiss the early reminders for the rest of the day.
Read by the shell
The local day, updated by a timer at midnight.
Worked out on every render
Pure, so running it again costs nothing but time.
Recorded after rendering
Once for each day the banner is visible.
The example leaves out saving the dismissal, trial dates that come from the server, and the time zone a billing system uses to end a trial.
Build UIs?Every component you write is asked to render without side effects, and one day an analytics count or a midnight rollover shows you why.
Where it already is in your components
React asks for exactly this split. Its guide says a pure function “minds its own business”
and gives the “same inputs, same output”, and that “By calling the component functions
twice, Strict Mode helps find components that break these rules.” The textbook banner
works out its message while rendering and records the impression in useEffect. React’s own analytics example runs twice in development, and the docs say to keep it:
“In production, there will be no duplicate visit logs.”
Svelte states the same rule for derived values: “The expression inside $derived(...) should be free of side-effects. Svelte will disallow state
changes (e.g. count++) inside derived expressions.” The Svelte banner derives
its message and records the impression in $effect.
When you have to own it
Now it’s the banner that stays open overnight. The only clock read is a small shell that
keeps the local day and sets a timer for the next midnight. bannerOn runs from
that day on every render, and the impression effect depends on the day, so it records once per
day the banner is visible.
Dismiss is a side effect too, and it lives in the click handler. React’s guide says event handlers “don’t need to be pure.”
export type Banner = { daysLeft: number; tone: 'info' | 'urgent' | 'ended'; message: string };
// Pure: the banner for a trial end date on a given calendar day (YYYY-MM-DD).
export function bannerOn(trialEndsOn: string, today: string): Banner {
const daysLeft = Math.round(
(Date.parse(`${trialEndsOn}T00:00:00Z`) - Date.parse(`${today}T00:00:00Z`)) / 86_400_000
);
if (daysLeft < 0) return { daysLeft, tone: 'ended', message: 'Your trial has ended' };
if (daysLeft === 0) return { daysLeft, tone: 'urgent', message: 'Your trial ends today' };
const message = `${daysLeft} ${daysLeft === 1 ? 'day' : 'days'} left in your trial`;
return { daysLeft, tone: daysLeft <= 3 ? 'urgent' : 'info', message };
}
// Pure: the calendar day a moment falls on where the person is, not in UTC.
export function localDay(now: Date): string {
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${now.getFullYear()}-${month}-${day}`;
}
// Pure: milliseconds from a moment until the next local midnight.
export function msUntilNextDay(now: Date): number {
const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
return midnight.getTime() - now.getTime();
}
// Stub for the panes: the app's analytics client.
export function track(name: string, properties: Record<string, string | number>): void {
void name;
void properties;
}
A banner worked out during rendering from its props, with the impression recorded in useEffect or $effect.
import { useEffect } from 'react';
import { track } from './components/analytics';
import { bannerOn } from './trial-banner';
type Props = { accountId: string; trialEndsOn: string; today: string };
export function TrialBanner({ accountId, trialEndsOn, today }: Props) {
// Rendering only calculates, so React can call this as often as it likes.
const banner = bannerOn(trialEndsOn, today);
// Recording the impression is a side effect, so it runs after rendering, not during it.
useEffect(() => {
track('trial_banner_shown', { accountId, daysLeft: banner.daysLeft });
}, [accountId, banner.daysLeft]);
return (
<p role="status" data-tone={banner.tone}>
{banner.message}
</p>
);
}
06 / Recognize it elsewhere
Anywhere a decision sits next to a read or a write.
You’ve met all of these. For each one, find the pure part and the effect at its edge.
| Where you’ve seen it | The pure part | The effect at the edge |
|---|---|---|
A reducer in useReducer | The next state from state and action | React storing it and re-rendering |
| A component and its effects | Rendering from props and state | Subscriptions, timers, analytics |
toSorted() and sort() | toSorted returns a new array | sort changes the one you passed |
| A Go HTTP handler | Working out the response | Reading the request, writing the ResponseWriter |
A test that fakes the clock or Math.random | Whatever the function decides | A hidden input the test had to replace |
When a test has to fake something the call doesn’t mention, you’ve found a hidden input. When a function can’t run twice safely, you’ve found an effect.
07 / Already in your toolbox
Your frameworks already ask for pure calculations.
Three places to look. For each one, find what must be pure and where effects go.
React · Keeping Components Pure
What makes a function pure, why Strict Mode renders twice in development, and why side effects belong in event handlers.
Read the guide ↗React · useReducer
A reducer “must be pure”, and in Strict Mode React calls it twice “to help you find accidental impurities.”
Read the reference ↗MDN · Array.prototype.toSorted
The copying version of sort(), which leaves the original array unchanged, and
has worked across browsers since July 2023.
A useful counterexample: a function whose job is the effectWhen splitting adds nothing
saveDismissal(accountId) writes one row and returns. There’s no decision to pull
out, so a pure core would be an empty function with a name.
08 / The parts to watch
Purity is easy to lose and cheap to fake.
These are the places it still goes wrong.
A cache is a hidden input
Caching a pure function keeps it pure only if the key covers everything the function reads. Keyed by account id alone, the banner stays at yesterday’s count, and it would miss a trial extension too.
Time zones are inputs too
toISOString() gives the UTC day. Decide whose calendar the banner follows, and
pass that day in.
Changing an argument is an output
sort() on a list you were given changes the caller’s list. toSorted() or slices.Clone keep the change inside.
Pure doesn’t mean cheap
Running a pure function on every render is safe, not free. Measure before caching, and key any cache by everything it reads.
The shell still needs tests
Moving effects to the edge makes them few, not correct. Check that the shell reads the clock once and records once, with fakes.
A double run in development is a check
React’s Strict Mode repeats rendering and effects on purpose. Don’t silence it; make the code safe to run twice.
09 / Make the call
What would you have to change tomorrow?
Give both banners a plausible change and follow the work it creates.
| The change | One function | Pure core and shell |
|---|---|---|
| A script that prints one banner | Short and fine. | Extra functions for one call. |
| Render it in a component | An impression per render. | One per view, from an effect. |
| Test “Your trial ends today” | Fake the system clock. | Pass the date. |
| Reuse it in a reminder email | Records impressions nobody saw. | Call bannerOn. |
| Switch analytics vendors | Edit the calculation’s file. | Edit the shell. |
Split the decision from its effects when it runs more than once, gets reused, or needs tests at particular times. A banner in a component is the moment.
Keep one function when it runs once and its effect is the whole job.
The question I’d leave beside the code is: what does this function read that isn’t a parameter, and what does it change that isn’t its return value?
10 / Take the idea with you
Explain the triple impressions without saying “pure.”
“Working out the banner also recorded that it was shown, and the page works the banner out on every render. We split them: the banner is calculated from the day we pass in, and the impression is sent once, after it’s shown.” In a review, the words are pure function, side effect, and functional core, imperative shell.
Before moving on, jot down why the impressions tripled, why the test depended on the hour, and one function in your own code that reads the clock or records something while it decides.
Connections to follow nextRelated lessons
- Values and references explains why changing an argument reaches the caller.
- Idempotency and at-least-once delivery is what to do when an effect itself might run twice.
- Closures and captured state shows another way a function can depend on something that isn’t a parameter.