Bootcamp
Custom states vs URL parameters in Bubble
Photo by Fer Troulik on Unsplash
Every Bubble app carries state that never touches the database: which tab is open, which record is on screen, whether a form is in edit mode. Bubble gives you two main places to keep it, the page URL or a custom state on an element, and the choice between them decides whether your app behaves like professional software or falls apart the first time someone hits refresh.
Get it wrong and the symptoms are familiar. A user walks away from a record, comes back, reloads the page, and lands back at the start. Or worse: a workflow reads a state a few milliseconds before another workflow finishes setting it, and a feature that works ninety per cent of the time fails mysteriously the other ten. Both problems are avoidable once you understand what each mechanism is actually for.
A URL parameter is a key-value pair appended to the page’s address (
?form=login&mode=edit) that any dynamic expression in your app can read. Because it lives in the URL, it survives reloads, works with the browser’s back and forward buttons, and travels with the link.
Anatomy of a URL parameter
Take a URL like yourapp.bubbleapps.io/auth?display=group1&mode=edit and break it down. Everything after https:// up to the first slash is the domain. The word immediately after the slash is the page name. Then the question mark announces that parameters are coming: the first parameter always starts with ?, and every subsequent one is joined on with &.
Each parameter is a key-value pair. Here display is the key and group1 is the value, and every parameter must have both. It’s surprisingly easy to type just a value, forget the key, and then wonder why nothing works, so always check you have the pair.
Two housekeeping rules keep your URLs sane:
- Stick to URL-friendly characters in keys and values: lowercase letters, numbers, dashes and underscores. Uppercase and spaces technically work, but the browser will escape spaces into
%20and the URL becomes long, ugly and hard to debug.some-parameter=i-love-building-appsreads; the escaped version doesn’t. - You can have as many parameters as you like: key-value,
&, key-value,&, and so on. But every one you add is something you have to keep consistent everywhere it’s read.
Values aren’t limited to text either. You can pass numbers, yes/no values, or the unique ID of a record in your database, which is exactly what serious SaaS products do. Look at Xero’s invoice screens: the list view has a clean URL, and the detail view appends invoiceID= plus the record’s ID. Bubble lets you follow the same pattern, passing a contact’s ID into the URL so a detail page knows exactly which record to show.
Reading and writing URL parameters
Reading a parameter takes four steps in any dynamic expression:
- Open the expression builder and choose Get data from page URL (typing “get data” finds it fastest).
- Type the parameter name, meaning the key, e.g.
display. - Set the type: text for simple cases, but numbers, yes/no and app data types are all available.
- Complete the comparison and type the value:
Get display from page URL is "group1".
That expression can now drive anything: show this group, hide that one, filter a search.
Writing a parameter is just as simple. Both the Go to page workflow action and the link element have a Send more parameters to page option. Tick it, add form = signup, done. One quirk worth knowing: when you type a literal value into these fields, Bubble often tries to offer you a dynamic expression instead. Click out of the field, click back in, and type the plain text; that usually settles it.
And here’s a detail that makes the whole pattern efficient: when a workflow navigates to the same page with different parameters, Bubble doesn’t do a full page reload. It just updates the parameter and re-evaluates the page’s logic. That’s what makes URL-driven interfaces feel instant.
What custom states are and how to set them
A custom state is a temporary value stored on a page element — any data type, including your own — set either by a default value or by a workflow action, and gone the moment the page reloads.
To create one, select an element (the page itself is a fine choice), open the Element inspector via the eye icon, and at the top you’ll find custom states with an option to add a new one. Give it a name, choose its data type, and optionally make it a list.
You set the value two ways: a default, or the Set state workflow action. A landing-page testimonial carousel is the classic example: a state called display number on the index page, defaulting to 1; clicking shape one sets it to 1, shape two sets it to 2, and so on. Referencing it in expressions is as direct as it gets, element name then state name: search for testimonials where item number is index's display number.
One organisational tip: put the state on the element where it’s obvious. The page is a reasonable home because it’s the top-most element and the easiest to find, but if a state belongs to one section, a nav menu or a card say, putting it on that element keeps things discoverable.
Custom states shine in exactly these simple, ephemeral cases: a yes/no flag for whether a menu is open, a number for which card is showing. Nobody needs a testimonial position to survive a refresh.
The decision: does this state need to survive?
Here’s the test that settles almost every case. Imagine the user reloads the page. Should they land back where they were?
If yes, the value belongs in the URL. Parameters persist through refresh. Not in the database sense of persistence, but in the practical sense: walk away, come back, reload, and you’re still looking at the same screen, even the same record. They also make the browser’s back, forward and reload buttons work, which users simply expect. An app where refresh throws you back to the start, because the current position lived on an invisible state, doesn’t feel professionally built, and no amount of visual polish fixes that.
URL parameters bring three more advantages worth naming:
- They’re easy to locate and reference. The URL is always there. Any expression, anywhere on the page, can read it. There’s no hunting through the element tree for which group is holding which state.
- They work cleanly with reusable elements. Getting data in and out of reusables is genuinely fiddly, but a reusable can always reach up to the page URL and read a parameter. (Bubble’s newer reusable element properties are the other good option here.)
- They pass data between pages for free. Add the parameter to the link or Go to page action and the next page has everything it needs.
If the answer is no, and the state is pure UI ephemera, a custom state is lighter and keeps your URL clean.
Guard the empty state with one redirect
There’s one gap in every URL-driven page: what happens when someone arrives with no parameter at all? If your groups only show when form has a value, a bare /auth URL renders a blank page.
The fix is a page-load redirect. Add a Page is loaded event with the condition only when Get form from page URL is empty, and a single action: Go to page (the same page), sending form = login. Now every visitor lands somewhere sensible. It’s worth putting workflows like this in a folder called Redirects so they’re easy to find later.
Use this pattern sparingly, though. Pages stacked with redirects become genuinely painful: every conditional evaluates on every load (burning workload units), the step-by-step debugger fills up with redirect noise before you reach the workflow you actually care about, and crossed logic can create infinite loops where two redirects keep undoing each other. Keep it to one or two per page, consider merging several checks into one workflow with conditional steps, and if the logic is growing, map the redirects out on a whiteboard or Lucidchart page before they map themselves out in production bugs.
Where custom states go wrong
The reason to be disciplined about custom states isn’t stylistic; it’s reliability. Picture two workflows firing at nearly the same moment: workflow one sets a state, workflow two reads it. If workflow two runs a few milliseconds early, it reads an empty value. The feature looks fine in testing, works most of the time in production, and fails just often enough to be maddening. Bugs that only appear ten per cent of the time are the most expensive kind to fix, and states passed in and out of reusable elements are where we’ve been burnt by exactly this.
So the working policy is simple: custom states for simple, self-contained UI logic; URL parameters (or reusable element properties) whenever data crosses a boundary between pages, into reusables, or across a reload. Don’t build practices that cause intermittent failures, even when they appear to work.
Try it yourself
Build a one-page experiment. Put two groups on a page, add a text that navigates to the same page with ?view=a or ?view=b, and drive the groups’ visibility from Get view from page URL. Then rebuild the same toggle with a custom state. Hit refresh on each version and watch what happens: the URL version stays where you left it; the state version resets. Five minutes, and the whole trade-off is in your hands, not just your head.
This skill in the AI era
The URL-versus-state question outlived Bubble entirely: every React or Astro app we build with Claude Code faces the same choice between route parameters and component state, and the answer follows the same reload test. It’s also one of the first things we check in a struggling no-code app, because intermittent state bugs are a signature failure we untangle in Bubble rescue work. A builder who can say “this belongs in the URL, that belongs in local state” gives an AI coding agent precise architecture to implement rather than vague hopes. It’s a clear example of the Bubble skills that transferred straight into AI-assisted development.
If this sparked something, let's talk.
No pitch, no pressure — just a conversation about what you're working on.
Let's talkRelated posts
Bootcamp
A simple approach to data modelling
A four-step method for designing a database schema: identify entities, map relationships, choose fields. Taught through the CRM data model built in Bubble.
Bootcamp
Module 5 — Application architecture: pages, states and reusable pieces
Single-page vs multi-page apps, URL parameters, custom states, reusable elements — and a big practical building a main template with an expanding navigation drawer.
Bootcamp
API calls as actions vs data (and the planning checklist)
When to use Bubble API calls as actions versus data sources, a one-to-many auth pattern, and the planning checklist that keeps integrations from failing silently.