How To Build A Good Bubble App (Not Quite Unicorns Style Guide & Development Philosophy)

Not Quite Unicorns Style Guide & Development Philosophy

This guide details an approach to building Bubble apps followed by Not Quite Unicorns, the youngest Gold tier Bubble agency.

:star: A version of this exists on a Google doc which you may find more readable: Not Quite Unicorns Style Guide & Development Philosophy - Google Docs

This guide may be updated, but the forum restricts edits after a while, so you can use the Google Doc as the source of truth.

Background

Not Quite Unicorns (NQU) specialises in saving apps from other freelancers, agencies, and citizen developers. Clients come to us when they hit a wall with their app, have bad experiences with agencies, or have built themselves into a hole and need expertise to get out of it.

As a consequence, we very often see how not to build a Bubble app - more often than we see how to build a Bubble app.

Our business model is a productised service. We do not bill hourly - we bill by task. That means that as an agency, our goals when building are to:

  • Build in a maintainable way
    • A maintainable way means a way that makes it easy to revisit, re-use, or modify a feature, without having to tear it up entirely
    • If we take shortcuts, we shoot ourselves in the foot, because it’ll take longer to iterate on down the line, when our client is still working with us
  • Build things the best way, not just a working way
    • Virtually every problem in Bubble has multiple solutions. It is up to us as developers to first know what those solutions are, and then evaluate the best one.
    • Never be satisfied with “good enough”, and you’ll thank yourself in the long run.
  • Build for the needs of tomorrow, rather than the needs of today
    • Linking the two above points, everything we build must be built with the client’s future in mind.
    • We do not build features in isolation. Features are designed to solve core business problems - it is solving these problems that we optimise for.
    • Therefore, even if the immediate requirements of a feature are fairly small and simple, we should build it in a way that supports simple future development.

We’ve done pretty well for ourselves by following these principles. The average Gold tier Bubble agency is over 5 years old. NQU is just over 18-months old at time of writing. We achieve this by retaining clients and showing them that we deliver good value. We are not the most expensive option on the market. Therefore, our gains almost exclusively come from an obsessive focus on building things in a maintainable way that makes it easy to change them in the future.

So, we are speaking from a place where we’ve proven that how we build works - on both clean apps, and when saving tech-debted apps.

What this guide is and is not

This guide is not intended to be:

  • A bible of the only, correct way to build things on Bubble
  • A claim that no better approaches exist than the ones we suggest
  • A course on Bubble basics or an exhaustive list of best practices

Rather, this guide is intended to be:

  • A proven, documented approach to building apps
  • Food for thought to develop your own best practices
  • A system on how to think about building on Bubble

This is not a best practice guide. This is a good practice guide - if you follow the principles and systems in this guide, you will likely end up with a good app. This is because the principles and systems we speak about here encourage you to think about Bubble in a certain way.

This guide is also opinionated and to the point. We think it’s the best way to build a Bubble app, but every app, agency, and client is different, so you may find some parts irrelevant or counterproductive for your own development style. We will make generalised recommendations, but there are always edge cases.

Let’s begin!

Naming conventions

Throughout your app, it is essential that at a glance, you can tell what you are looking at, and what type of data you are dealing with. If you’re familiar with TypeScript, this is the kind of feature you get in an IDE - it is easy to know whether you’re dealing with a text, a number, an object, or else.

It is important to accomplish the same in Bubble.

Elements

Sometimes, no naming convention is simplest.

Now, it may sound absurd opening a build guide with such a statement. However, I will explain the rationale behind it.

Bubble has, implicitly, instilled a universal naming convention upon us for front-end elements. You know this when you place an element on a page. It will default to Text X, Group Y or Button Z, for example.

I do not see any value whatsoever in naming every element. There’s value for you as a developer if you’re paid hourly, but it does not deliver value to you as a developer, other developers that might work on the project, or your client. There might be a claim that it helps with readability, but I think that’s very limited, and if you do modify element prefixes for readability, you likely don’t do that for every element (and therefore aren’t disagreeing with my core argument that we do not need to rename everything).

Examples of naming conventions for every element which hold little semantic or other value include:

  • Prefixing all groups with GR_[group name]
  • Adding emojis into names
  • Removing the element type entirely

Now, there are a few reasons why:

  • Most groups are functionally irrelevant and serve no ends other than being a way to structure the UI
  • Using a specific, complex naming convention makes it harder for a developer unfamiliar with your project to begin working on it
  • It can make search more difficult, as when most developers are used to searching for Button [x], if you’ve renamed all buttons to BTN - [x], it takes away that muscle memory.

Now, there are some things which I think should be named.

As a rule of thumb, if the element has a workflow or some relevant logic, it should be named so that it is clear what it does. That means that, for example, almost all Buttons should be named e.g ‘Button Create Account’ or ‘Button Make Payment’.

Additionally, key UI elements/groups should be named usefully. For example, you may name the header ‘Group header’ or a card ‘Group user card’. However, it generally makes sense to keep the prefix (the element type) as-is, so that other developers know what they’re looking at.

Custom states and group variables

Against custom states

We do not recommend using custom states at all. There are a few reasons for this:

  • They are functionally inferior to group variables, as they cannot be set dynamically in an elegant way
  • They are not as visible as group variables in the editor

In almost all cases, anything a custom state can do, a hidden variable can do better. One notable exception is passing data between reusables.

In defence of group variables

A group variable refers to making a group or repeating group have a data source. That is not to display anything, but is to store a dynamic variable that you can later reference. For example, if you reference an ‘Invoice’ in multiple places on the page, it makes sense that all references to said invoice use the group variable.

We recommend naming group variables as var - [name]. This makes them easily searchable and distinct in your scan flow. It doesn’t necessarily have to be var exactly - VAR might look cleaner to you, and that’s okay. It should, however, be short end easily searchable. var is convenient to type with one hand on the keyboard.

  • Group variables are highly visible and can be dynamically set
  • Group variables can be placed globally (e.g. in a hidden floating group), or locally (next to where they are used)

Storing hidden variables in a popup or floating group is functionally identical with the caveat that plugin elements generally do not load in a popup, so it would be advantageous to use a floating group. That said, I’m normally in the habit of using a popup which is fine for most use cases, so you do you.

Let’s unpack what I mean by placing a group variable globally or locally.

Suppose we have an invoice page, where we display the invoice’s line items, and also some customer details. The customer details are collapsed by default.

In a popup, we would have a hidden variable var - Invoice. This references the invoice we’re displaying, and would often come from a reusable element data source or property. Also in the popup, we would have var - Line Items which is a repeating group that stores the line items to display. The search constraint would be Do a search for Line Items where Invoice = var - Invoice’s Invoice.

These are global variables that dictate the entirety of what the user sees, hence they’re positioned in a hidden popup where they can all be viewed and managed together.

Now, let’s consider the case where we want to monitor the expanded state of the customer details. I may have a hidden variable right next to the customer details card, called var - customer details expanded. Why? Locating the hidden variable geographically close to where it is used/updated can assist understanding its functionality. You can judge which location for the particular hidden variable is right for you.

Naming pages and reusable elements

I do not have any particular system that I think is best for naming pages and reusable elements. As long as they’re named consistently, it is likely valid.

Take this page manager screenshot from NQU Secure:

You will observe a view things:

  • All popups are prefixed with popup. This makes it easy to find any popup. Additionally, all popups in this app are reusable elements. I think every app should use this practice. More on that later.
  • Specific UI components like cards are named as such e.g cardCheck, cardApp
  • Non-specific components like componentAdmin and componentApps which show the admin panel and the user’s apps respectively, are prefixed with component, for lack of better options. I do not currently have a better approach to recommend here but will update the guide if I find one.

Data structures, workflows, and calling a spade a spade

Now, naming elements is only part of the app. Based on my experience, the naming convention (or lack of one) of front-end elements doesn’t affect the maintainability and readability of the app too much.

It is in the logic and your data structures that naming conventions do become non-negotiable.

The naming convention I will show you is based on the following principles. Names must:

  • Communicate what it is for
  • Communicate what type it is
  • Be easily readable and plain English (or whatever language you develop in)
  • Do not overcomplicate - call a spade a spade.

So, here is what I recommend:

Data types, option sets, fields, and attributes

All data types and option sets should be in Title Case and singular.

Good examples Bad examples
Invoice DT_Invoice
Invitation user_invitation
User Role OS - User Role
Product ProductID
Billing Account stripeBillingAccount

Some people will claim there ought to be a distinction between data types and option sets e.g with a prefix. I do not find this convincing. It will almost certainly be self-evident given the app context whether the type you are dealing with is an option set or data type. If there are two similarly named data types/option sets, then yes, clarify that in the naming of them.

So, this clarifies naming for data types and option sets themselves.

Whenever we reference a type (i.e, another data type or option set) in a field or attribute, we use that type name exactly.

When we reference a basic data (e.g date, text, number), we use camelCase with no spaces.

This is best demonstrated with a screenshot (this particular one is the Check data type in NQU Secure):

Now, a few observations:

  • All basic data types are camelCase
  • All links to other data types are Title Case matching the name of the type we are linking to
    • There is an exception which is Associated Fields. Often, it is necessary and beneficial to add more clarity as to what a field is used for. For instance, in NQU Secure, we have a list of Verified Apps and Pending App (both lists of App data type).
  • Booleans (yes/no) are generally prefixed with isX.This is because it is readable, and makes linguistic sense in a Bubble expression (e.g This Check’s isNew)

Workflows and logic

The real power of this convention comes in workflows.

We title all workflow parameters in the same way. Our approach heavily uses custom events, backend workflows, and reusable element properties. That means passing data between functions frequently.

Let’s take a look at this custom event:

You can see that wherever possible, we name it exactly the name of the type of data we are dealing with.

Inside the workflow, this makes it extremely easy to know what type of data we’re dealing with. Of course, when editing expressions, Bubble indicates to you what kind of data you’re dealing with, but by naming in this way, you get a much quicker intuition for how the workflow works and what it does.

So, let’s look at how that’s applied:

Would you look at that! It is intuitive which inputs map to which fields, because of our naming convention. User = User, App = App, and call a spade a spade.

The purpose and type of each field is clear, and it links to our database intuitively.

Data types and option sets are Title Case - they look big. They are proper nouns!

Basic fields are camelCase. They are, well, basic, and simple.

Backend workflows and custom events

Much like naming reusables, I don’t have as many hard and fast rules with respect to the naming of backend workflows and custom events. Personally, I go for Title Case. I used to do kebab-case-like-this, but reflected on it - what’s the point? I want names to be readable, not technical. To me, Complete Audit Processing seems much more readable than complete-audit-processing. In addition, going back to this ‘proper noun’ idea, it gives the custom event/backend workflow a bit of weight. Perhaps that sounds strange to you - I don’t know if other people feel the same way. That’s why I’m sharing it!

Security

Privacy rules should be applied as soon as a data type is created (or as soon as practical)

Applying privacy rules immediately ensures that you structure your database in a way that supports privacy rules. In addition, adding them later leaves a lot of room for bugs to creep in, as bugs relating to privacy rules can be hard to track and debug.

Privacy rules are the only thing, without exception, that protect data read access. You must configure them, and they should be the first thing you do as soon as you create a database structure.

Backend workflows should be public if and only if necessary

Public backend workflows are fairly easily exploitable from a security standpoint. They used to be public by default, but fortunately Bubble has recently patched this.

Go through your app and ensure that only backend workflows that need to be public are public. A backend workflow only needs to be public if it’s being called via the API.

This might be, for example, for a webhook that’s received from Stripe, or an API that you call internally via the API connector.

Anything that lives on the page is accessible to a client

This isn’t so much a best practice as it is an FYI, but anything that you put in a workflow or on a page, on any page, regardless of whether it’s protected by a redirect or not, is publicly accessible. Do not statically define API keys in plugin elements on the page or in workflows.

Remember that any text, for example, AI system prompts that live in a reusable element or a page, are going to be exposed.

You should move it to the backend if you do not want it to be exposed.

Workload

Excessive focus on workload hurts clients and maintainability

This issue frustrates me so much. I see some developers investing so much time and effort trying to squeeze every last workload unit out of their project. However, I don’t think this is beneficial for the app, the developer, or the client.

Do things in the right way, in a way that makes things easy to change in the future. It is not worth spending $100 of development time to save a client $1 a month in workload units. Only optimise workload issues when they become an issue.

This does not mean it is acceptable to not use best practices with regard to workload. It just means not going above and beyond to make the primary focus of your development be reducing workload costs. If you build in a way that makes things easy to change, then it won’t be a problem to easily optimize in the future if necessary.

Take a marketplace which shows a list of products. This will consume workload units when someone loads the homepage.

If the client starts getting 50,000 views a day, then the costs of such search will likely start being significant, so it might make sense to refactor that into a satellite datatype to save the workload. But this should happen if and only if the workload savings are worth more than the development time plus any maintainability cost associated with the change.

External databases should not be used with Bubble*

Following on from the above, no app that is entirely built on an external database should be built on Bubble from the start. Bubble is a full stack platform. It is a bad front end and it is a bad back end. It is a fantastic full stack developer experience.

If your project requires an external database, it should not be built on Bubble. Bubble is a bad front end and is clunky to integrate with external back ends. You are better off using a tool like WeWeb, which is purpose built for this use case and is objectively a better front end.

If you try and sell a client a new Bubble app with an external backend, you are not selling them the best option. You are selling them an option which is convenient. Bubble is where your knowledge lies. You are failing the client by not recommending them that better option.

In existing apps, there may be cases where a small number of tables may benefit from being in an external database. However, this is rare and should only be done as a last resort.

This should only be done if the cost-benefit analysis, when factoring all in, including the cost of the external database, the cost of the additional development time to implement it, and the cost of maintainability, indicate that this is the best option for the client.

Satellite data types should be used sparingly

Satellite data types add complexity and maintainability challenges to the app. There are occasionally good uses for them, but use them very sparingly, particularly when the only reason for their use is workload savings.

If it provides significant performance savings in terms of speed, then that’s a more convincing reason to use it.

For example, in a recent app, we had a file data type which stores files the user uploads to chat with an AI. Originally, we stored the content of the file on this data type, but that obviously meant that when we used the file browser that we built, all of the content for each file had to be downloaded to use that.

When we saw the sizes of the files that people were uploading, we moved the content to a separate file content data type, rather than being on a content text field. This way, we only need to load the file content when we want to.

Modularity

Don’t pass data between multiple parent groups

A common setup in a repeating group, for example, a repeating group of tasks, is to have the cell and then in its child cell reference parent group thing, and in that child cell reference parent group thing, etc. This is a pain.

If you want to change the type of the repeating group for the purpose of copying it to another part of the UI, or just a change in data structure, you have to update every single group’s type and reference.

Instead, in a repeating group, the top-level group should be a group variable, which is the thing for this cell. Then all references to that thing inside the cell should reference that hidden variable rather than the parent group.

Here is an example of a good pattern:

The only common exception is when the parent group data is required for auto-binding.

The default ‘Creator’ field should never be used

The default creator field is a pain and really causes trouble for us when we take on apps. The challenge here is that you cannot manually assign or override it.

For example, if we receive a webhook or we call an internal API with an admin API token, then the creator field will be the admin rather than the user. This limits how much we can build in the backend and makes it near impossible to reassign data to a different user.

For example, if we have a project management tool where a task has an owner which is a user, bad apps will use the creator field to say who owns it, because that’s who created it. However, it should have an owner user field that can be manually changed.

Privacy rules, etc., should always reference this manual field rather than the default creator field.

Essentially, pretend the creator field doesn’t even exist.

All popups should be reusable elements

All popups should be reusable elements. Each popup should have a custom event to open and a custom event to close. Closing the popup simply hides it.

Opening the popup uses the custom event to take any parameters that the popup requires or can optionally take. It then displays those inside hidden variables inside the popup as desired. Then it shows the popup.

The reasoning for this is simple. It means you can just drop a popup on any page. You can trigger the popup using trigger custom event from a reusable element workflow action. This custom event will contain all of the parameters that you can provide to the popup. As a developer, this means you know exactly what it takes and how it works so that whenever you place it on a page, it will just work.

We recommend against using data sources of popups and instead take the data from the custom event that opens it. The reason for this is that it’s easy to forget to assign a data source.

By having all data that a popup requires pass through a custom event when it’s opened, it’s clear to a developer exactly what this popup needs or can take as an input.

The additional setup is minimal, and the maintainability gains are enormous.

Think about reusable element scope and responsibility

One pattern that I see in apps is a reusable element for all, for example, delete actions. This popup will take a bunch of different data sources, of which only one is ever used, and will delete it. For example, this one delete popup might handle deletions for invoices, payments, chats, profiles, etc. The reason this happens is because the developer sees all of these functions as being related by virtue of them being delete actions. Therefore, they can be put into a reusable element.

However, I do not think this approach is particularly elegant. I find this diagram useful:

Essentially, everything that we build should take one set of inputs and return one set of outputs. This is known in code as the single responsibility principle. It means that we always know what something takes and what something does.

Everything in the app is in small reusable components. The delete pattern I described above is based on the first example. There are multiple functions inside the popup, and the response is different in every use case because there are different delete workflows for each thing.

The approach that I would recommend for this deletion example is associating all of these actions, not by the fact that they are deletion actions, but that they are confirming something. To that end, I would have a confirm action popup. This popup has reusable element properties for the title and description.

Inside the popup, when it is confirmed, or if it is confirmed, we set a state on the popup which says confirmed is yes. Whenever we want to use this popup, we place it on the page.

Let’s say that we want to use this popup. Suppose we’re on a profile and we want to delete the profile. We click the delete button, trigger this confirmation popup, and then we have a listener on the page.

Do when this popup’s confirmed is yes. When it’s confirmed, we run our deletion logic. Now, someone might initially say that that means that the logic is on the page so we can’t reuse it. That’s correct, which is why it’s wise to put it into a backend workflow so that it is reusable across the app.

Geography in your app

In addition, keeping the delete logic geographically close in your app to where it is used, such as in the profile itself, rather than in a dissociated popup, means it is easier to semantically understand how your app works.

I’m not sure how much this concept of geography is intuitive to other people or if it’s just something that I picked up. But I do feel that it makes sense to keep logic close to where it is used. The same principle applies in traditional code.

Reusable element properties and custom events are your friend

Reusable element properties and returning data from custom events were single-handedly the best features that Bubble added for maintainability since the responsive engine. They are now significant proper use of modular functions.

Each reusable element and custom event can have its defined narrow purpose so that each component only does one thing and does that very well. Now that we can return data from custom events, it is easy to have reusable logic that lives in a global reusable element or directly on the page.

Make extensive use of the backend

Lean towards using backend workflows as it makes the app so much more maintainable. Backend workflows should be clearly organized into folders. You may have a preferred naming convention for backend workflows, which additionally assists with organization.

Every app should have backend workflows for actions like signing up a user. This workflow runs and takes a user parameter, which is their created account. This might be used to create data things and tie them to the account or send emails, etc.

Every app should also have a send email backend workflow which takes the content, the person to send it to, the subject, etc. This way, all of your email logic passes through one place. Should you change provider or change the API call, you only need to update it in one place.

People see the backend as a downside because it’s asynchronous, but that’s okay. Most things can be asynchronous.

The backend is pretty fast, and generally when you schedule something, it will generally run immediately.

Closing thoughts

I hope that offers some food for thought. As I mentioned in the beginning, every app agency and developer is different. This is just a system that we’ve proven to work on well-built apps and poorly built apps that have been handed to us.

There are edge cases for everything, so a rule of thumb is never a black and white rule. Hopefully, there is at least one thing that you can take away from this that you find useful.

22 Likes

Thank you for the detailed report, George.

I do have a question though with the quote I added from you.

I have an apartment app where when the apartment listing is created, there are approximately 30 options that the apartment may have (stove, refrigerator, pet friendly, etc.).

These options are stored in an option set.

There is a custom state that stores the appropriate options when the listing is created and then saves them to the database when the ‘create listing’ button is clicked.

Of course, when the apartment is edited, that custom state also adds those options to the listing and then changes according to the edit.

You’re saying this is not a good practice?

I’m not quite sure I understand what you mean about variables and how to use them.

1 Like

Great read, and heaps of food for thought (+1 for the google doc), thank you!

1 Like

I would instead use a hidden repeating group that is of that option set’s type. You could use a multi-dropdown and in the repeating group reference the multi-dropdown’s value. That will ensure that the hidden variable is always equal to the drop-down value.

If for UI reasons you are displaying the list of options selected in a repeating group, the same thing applies. If you want to add something, add it to the list in the hidden repeating group and if you want to remove it, remove it from the list in the hidden repeating group.

This is functionally identical to a custom state, but it’s so much more visible because it’s actually present on the page rather than the list itself. So I’m not saying that custom states are a bad practice at all. I’m just saying that I think hidden data variables by setting a group or a repeating group to the type of data you want to store are probably a better practice and will make life easier for you as a developer.

1 Like

Thank you.

I have a repeating group that shows all the options on the create listing group.

Once the property manager clicks the option, it’s saved in the custom state.

Here’s an image of what I mean. I couldn’t get all the options in the image.

I’ll look into your idea though and see if it helps.

1 Like

Totally agree. I’ve found satellite data types get a lot easier if you use database triggers. Honestly surprised you didn’t mention those; they’re really handy.

Maybe I’m missing something. Is the main benefit here that you don’t need to update every group if something changes? I usually just set the datatype at the parent container, then let child elements reference their direct parent. Seems straightforward enough.

:100:

It honestly took me forever to realize how useful this update was. Passing data back from reusable elements to the page felt weird at first. Now I’m at a point where almost everything is reusable. I use custom states inside reusables as the default for their properties, which makes it easy to control and pass data around between the element and the page. Basically, almost everything I thought wasn’t possible with reusables actually is. My apps are now packed with reusables nested inside each other. Like, a company page is reusable, its table is reusable, the cells are reusable, and so on.

1 Like

The problem with this is that if you want to copy the UI to use it elsewhere/change the type of data (this is related to the principle of making future development easy), you have to update every single reference through multiple layers of groups. Much easier when there’s one reference to the Current cell’s Thing at the top level of the RG cell.

Thanks for sharing these strategies, George!

I couldn’t agree more about building with maintainability in mind, it is crucial. No app is ever truly “done,” right? Being intentional with naming conventions, both in groups and the database, makes a big difference in the long run. In my opinion, though, taking just a few extra seconds to name groups pays off in terms of organization.

On reusable elements and custom events : they are most definitely our friends! I’ve worked on a few apps that were already up and running, and it gets tricky when you run into multiple groups or popups that could have been a single reusable. It often means updating the same change in three different places instead of just one. Plus, working with parameters in reusables is such a smooth experience! I use those a lot.

The part about hidden variables vs. custom states really caught my attention. I usually use custom states for things that need quick responses on the front end, like filters, or for values that will be saved later in the workflow, but not as actual data sources. I’ll definitely dig into the document you shared, thanks a lot for including that!

Also, the backend is definitely our ally. So many actions can run asynchronously, and handling them in backend workflows not only improves organization, but can also boosts security! And thanks bubble for the update on API workflows not being createt with the “expose as a public API workflow” checked by default anymore.

One thing I’d add, with the new workflow tab, folders are more essential than ever, especially in SPAs. Navigating through unorganized (or worse, absent) folders can be a real challenge.

All in all, great content, really appreciate you putting this together. Lots of good food for thought!

2 Likes

Hey @georgecollier , I’m with you on 99.8% of your best practices! The only difference is that I prefer using snake style for naming data types (e.g., Billing_Account) — I find it makes things more straightforward if the table ever needs to be exposed via the Bubble Data API.

As for option sets, I like to prefix them with OS to make them easier to identify and locate within the app. In my experience, it doesn’t cause any harm and adds a bit of clarity.

Appreciate you sharing all this great material!

3 Likes

Very great tips here! Especially regarding Reusable element properties and custom events. Lots of builders think having 1000 actions in one workflow with giant conditionals is a mark of success or something. What it is, is a badge of shame.

I appreciate all of your work, but this is where I’ll have to put on my gloves /s

States

Firstly, if i were to replace states with groups I’ll have tons of group and RG elements pointlessly rendered on frontend.

Secondly, states were designed to be used in workflows. States in Bubble are “absolute” as you cannot accidentally change their values anywhere but with a WF action.

State lists are also static in nature, so it gives you absolute control of the data you load. Not everything is about WU, data control helps UX.

They serve very different purposes and should not be viewed as competing functions. I too use group variables for dynamic data but again, not everything needs to be dynamic.

Popups to Store Variables

I lean more towards floating groups as I’ve had issues with using popups to store my states and plugins.

External databases should not be used with Bubble*

This is very shallow thinking. Bubble’s DB is good at what it does, but it does have limits. These limits includes:

  • strict transactional requirements.
  • speedier search
  • vectorized data
  • audit requirements
  • integration with other stacks

I do agree that some builders don’t actually need to use an external database but there is nothing wrong with stacking an external database. I built my own tech stack to support my apps and technically I can not use Bubble but I am more than happy with how Bubble integrates. It’s only clunky if the implementation itself is bad.

Continuing my apps on Bubble while stacking my own tech hasn’t detracted from the UX so why switch?

All tech rely on other tech. That’s why they are called tech stacks. I like the ease of building on Bubble and contrary to what others say Bubble is totally scalable, if you build right.

In Summary

There are no hard and fast rules in tech, only best practices. Everything serves a function, it’s about using the right tools for the right job.

5 Likes

This is good practice for custom data types, but not option sets. The reason for coming up with hidden variables was to load more data onto the page to make processing data a bit faster, and this practice of hidden variables came about prior to option sets.

When working with an option set for filtering and using custom states for storing those filtered values, as you say you approach is functionally identical to a custom state, it is likely a better practice to use the more built in approach of just using the custom state itself, rather than adding the complexities of incorporating a repeating group and floating group to keep it hidden, because at the end of the day, how are you adding the item to the list in the repeating group? You are having to do a lot more work to add the option into a repeating group than you need to do to just add/remove it from a custom state that is a list of options.

@senecadatabase if what you have is already working for you using custom states, do not change it. There is going to be no benefit from using the approach of a repeating group for your use case, in fact, it will just complicate the setup and add more elements to the page (ie: just a slight uptick in the amount of data being sent from Bubble to your page in order to load it).

If though, you were looking for a way to improve upon custom states for the purposes of adding in some type of functionality, consider using URL parameters or Local Storage. Both URL parameters and Local Storage have benefits over custom states or a hidden RG tied to a custom state (because how else are you adding/removing items from the hidden RG if not via a connection to a custom state?) and the reason to use one or the other comes to the type of feature/functionality you are aiming to achieve.

In your app of an apartment management app when creating a listing, you want to use Local Storage as it allows you to store much more information and related information. Additionally, you want the functionality to allow somebody to start a listing, not finish it, and not save it and still come back to it as it was in order to ‘start where they left off’…large platforms like Booking.com have these types of features baked into their platforms because people find them beneficial since the creation of a listing may take a decent amount of time and the user may not have all necessary values ready and so may start and need to come back.

URL parameters are helpful for your search page where users will filter, and you can store in URL parameters the filter choices, so that when they navigate from the search page to the listing page, and later use browser back button to return to the search page, the URL parameters are still there, meaning all their filter choices are still as they were, as a user would expect them to be. Using custom states or hidden repeating groups tied to custom states do not do that.

In Bubble there are always going to be various ways to approach something, and the features/functions and type of app being built should be the guide to which approach to implement as it gives a much clearer basis for which path to follow.

1 Like

No doubt! That’s why I made sure to say exactly that!

I get that - you can make a dynamic data variable static using Display data / Display list to accomplish the same thing!

But, the killer benefit of group variables is how visible they are in the editor.

I think my issue is more with people building apps for clients and not being open about the fact that better options exist other than Bubble if using an external DB. If you’re like, yeah Bubble is fine with external DB, tool X is better, but we only work with Bubble then the client gets that info and can make a decision. But I don’t know why people are offering to build new Bubble apps that are solely Supabase/Xano backends - like, just use code, or a purpose built front-end tool like WeWeb.

Nope, not at all. This is a secondary effect.

Group data variables are for maintainability - specifically, not repeating yourself. Correct use of them is probably the biggest predictor of whether an app we get handed is gonna be fast to work on!

1 Like

I see your point. It’s probably because I am uninformed. I have the privilege of running a profitable business that does not involve tinkering with apps built by others and actively avoid doing so.

I do agree that it’s always good practice to see how far you can keep to native Bubble before moving to using/building plugins, custom code or external services.

2 Likes

Very nice writeup! I do like 90% of this. Especially agree with the WU bit. I know hidden variables are technically a bit more flexible than custom states and I do use them sometimes but custom states just feel lighter to me (placebo) so I tend to use them more often than not. But the “set state” action is completely interchangeable with “display data.”

I use a custom state to set the “type” of popup and then reference that in conditionals, slightly clunkier but same general concept.

Very hot take but assuming:

  • Functionally there will NEVER be a reason to “let someone see” the information
  • It’s sensitive

I see Creator being hardcoded as a feature, not a bug. It means there can never be an improperly set up workflow condition that will ever allow it to be changed. I do see how this can be a problem for many use cases though.

Also custom events are the most underrated part of Bubble, especially being able to return stuff conditionally. It is essentially if/then branching. I like using them as the very first step of a BE workflow for validation purposes since they’re guaranteed to run before everything else.

It’s just adds a bunch of API calls right? Essentially everything becomes an API call instead of a “make changes to a thing.” Not sure how different it is to “delete a field” on Supabase or Xano vs Bubble.

2 Likes

Yes - imagine a scenario where you do a table restore from a backup (or for any reason) the creator field is wrong and all the privacy rules are broken.

Bubble are very reticent on proscribing best practices - George is doing a GREAT job! :slight_smile:

1 Like

I think the main reason George is so against custom states is because from an agency point of view it would be literally impossible to find them jumbled up in random groups.

If the average person was very disciplined about how they organized them, it wouldn’t be on his radar.

2 Likes

Well more specifically, group variables offer a superset of features that custom states have, so use group variables, and they’re easy to find and use.

My only hesitation is the same reason as @ihsanzainal84 which is adding to the DOM tree (even if it’s invis/etc.). I’m not sure if this has a practical difference in performance but it just feels like it might.

Of all of the performance factors you can control, I promise this is not one that’s worth spending your time on!

There’s an unusual fascination with reducing the number of elements on a page in this forum, which is only really necessary for absolutely humongous apps. When most of them are invisible, the only way it slows the browser down is by increasing the size of the initial request to load the page as the JSON for that page is larger.

When a single element is just a few bytes, it’s really trivial.

You can try it out for yourself if you must. Try getting an empty page and testing the average page load times. Then, add 1000 invisible elements - you won’t notice much difference.

Even if it did affect page load times, I’d prefer an extra 0.01 seconds if it saves me hours when I come to revisit that feature and attempt to update it.

2 Likes

What is that feature set?

In terms of the use of custom states, for me, what I found over the years building on Bubble, is that I would tend to forget which element I put the custom state onto, because when I first got started, I thought it best to place the custom state onto the same element from which it would need to be used, but would always forget, so now in my old age, I ensure my memory doesn’t disrupt my flow, so I place every custom state for a specific page or reusable element onto that page or reusable element itself.

Not only does that make it easy to look when in the editor to see easily all custom states in one place, it also when working with custom states in reusable elements it ensures that the custom state is accessible for actions from outside the reusable element to trigger (ie: when RE is on page and page has button that will trigger the change of the custom state value -it is not possible to target the custom state of a child element within the RE).

I think the difference in thought maybe in line of ‘do I build an optimized app’ or ‘do I build an app I find easier to build or work with’, and if choosing the latter, the extra costs of extra elements is insignificant, and so gaining just that extra slight advantage to build in a way you prefer not to is inconsequential, so better to just build how you personally feel comfortable building in.

I was taking a more historical perspective on the approach. It started in 2017. I can not speak to the mindset of the people who first put it into the Bubblesphere, but I believe it was in mid 2017 as I first came upon the use of the ‘var’ in popups for creating a list of dates (back in mid 2018) LINK to FORUM POST.

And I would say that most likely it was not introduced for maintainability for a couple of reasons. Firstly, I may be incorrect about, but I don’t think Bubble had reusable elements until around June 2018, so more than a year after when I believe the ‘var’ in popup first started to be around. Secondly, they do not really help with maintainability (unless it is a reusable element that is a global data center), they just add to the time it takes to track down where is the true source of the data, and that is because the repeating group on the page is displayed to user, so when you see that as incorrect, your first attempt and instinct as a developer is to search for the RG that displays the values, and when you find it to uncover that it doesn’t hold the search and instead references a ‘var’ data in a popup, you then need to find that popup and ‘var’ source, which just simply adds to the amount of time it would take to get in and find what you need to find.

However, the use of ‘var’ data (or just hidden data sources) is that they allow you to ensure data is getting loaded how you want to in order to help improve the performance of the application in various ways, such as loading a shorter list first and then loading in the hidden group more data after initial load. It was also mostly incorporated into SPAs that would need to load large amounts of data upfront that due to the nature of SPA, once loaded wouldn’t be reloaded, which made it easier to replicate the ‘snappy’ behavior of native mobile apps because ALL the data was already loaded.

I’m confused :confused:

Completely agree. Naming conventions that are followed make it so easy to find things. Getting into a client app and being able to quick type to find the element or data field etc. can only be achieved with good naming convention.

Agree, keep the name of the element type intact, and when using the replace feature for replacing an element by type, ensure you change the name as Bubble doesn’t do that automatically for us. One thing I have started to do more though, is add a suffix to the end of something like “Group Header” to be “Group Header Popup Delete” as often enough in the inspector tools, trying to find the correct Group Header or whatever element, you may have multiple and not know which you are actually trying to inspect.

I personally do not use custom states much in apps, so I don’t advocate for their use, but I don’t really agree with this as I don’t know what you view as benefits of a hidden variable like a RG versus a custom state as a list other than it is easier for you to see the groups in the editor, which is a valid point, and a great reason to use them for that benefit, but otherwise, I don’t see the benefits of hidden variables over custom states.

However, I do agree that making use of them to store a value you will use repeatedly in dynamic expressions is a great use of them. I got started doing this years ago and it really saves on time with crafting dynamic expressions. One other thing I do now, that I has lessened my use of hidden groups as data sources, is I don’t use them any longer to store numeric or text values and instead use an input element, especially for checkouts or other form types that require calculations.

I use my-page-name so that it is not going to get turned into my_page_name by bubble because for me personally, when sharing a link, with underline text to highlight visually that it is a clickable link, the underscore is lost to the underline, or at the very least, easy enough to ‘miss’ visually, and so just for the sake of ensuring I have easy to read, easy to print, easy to share links to my app, I prefer to use my-page-name

For reusables I prefer to use Capitalized Words like Reusable Element, because Bubble doesn’t automatically change it into capitalized_words like they do with pages, so it adds just an extra visual cue to me when I look at the list of pages and REs that it is is an RE, and it also lines up with how I name my other elements like Group Header, so I like seeing it automatically applied like that when I place the reusable onto my page.

Suffixes of Popup or Group work better for me as it keeps my reusables that relate to the same data together in the list, so I can see everything in terms of reusable elements that that data type might have, such as ‘Product Crud Group’ or ‘Product Filter Group’ etc.

These should all be in what-this-case-is because of so many reasons, like for one, with data types if you use the API with, it is better to use kebab-case.

This started to be done before bubble began to group the data sources for us. So in the past, all custom data types and option sets were in just one grouping sorted alphabetically, so in order to basically group them ourselves, people began to implement the approach of a suffix of either Option Set or OS in order to ensure all option sets were grouped and sorted alphabetically while custom data types were grouped and sorted alphabetically. It was a strategy implemented to work around a lack of features from Bubble, that thankfully Bubble has implemented. But for me personally, I still set a prefix of Option Set as it just makes it easier to know that it is an option set because if you have a field on a data type that is an option set, there is no way to know and distinguish if it is a custom state or option set unless you either name the field itself or see it next to the field.

they come to form URLs used sometimes in APIs and webhooks

Bubble forces backend workflows to be named without a space, so in terms of readability, just pick your poison of some case structure, but as mentioned above, reason for kebab-case.

I completely disagree for so many reasons, but I’ve shared enough in the past on that, and so it definitely is a personal opinion.

Wrong.

This seems to be a failure in understanding ways to save workload units, and not necessarily experienced guidance for others to follow. I would say at this stage, my ability to save clients thousands of dollars a year on workload units for some app types or enabling a monetization strategy to be scalable on Bubble in the age of workload units doesn’t cost $100 per $1 saved. I’ve personally already invested my own time and energy into upskilling myself for building in Bubble in the age of workload units, and so it comes at no real extra cost to the client. And the ways in which WUs can be saved, in some cases, has literally taken what was a two hour task done to 20 minutes at most.

The above contradicts the below…

Definitely a personal opinion, but I 100% agree on the idea of being a decent person and not steering a client wrong just to get the project.

They do not save workload units unless it is an extremely heavily fetched data set because of the additional workload unit costs of having to maintain two separate data types of the same type. There are now much better approaches to satellite data types that do have considerable workload unit savings and other benefits.

Great point. I remember a tip post about this from a while back.

Yes, I call mine ‘Selector’ rather than ‘var’ as it is not really variable, it is the actual cells thing, and for me in my apps I generally select a thing from the RG.

I had to start doing that a long time ago, for the main reason that the feature of cookies and a non-logged in user creating data, once they register within the 72 hours, bubble would assign them as the creator of the data, but in situations in which the user was already a registered user, but just not logged in, when they go to login, they do not get assigned as the creator.

I disagree. And I believe that for the most part, unless you know 100% that the reusable element will only ever be a popup, it is best practice to create the RE as a group, because you can then later place the group into a popup or floating group or onto the page directly, resulting in a much more flexible and therefore more modular approach to using reusable elements.

The examples of user signup and emails are very good examples for things that should be done in backend workflows. For me, not really for maintainability as much as improving UX as when a user signs up instead of running all actions on client device we can offload most to backend which enables the user to not really be slowed down by the app having to run some workflows.

Thanks for putting this all together, it is great to point out ways in which you prefer to build and why. Hearing other developers thoughts on these types of things is great and really helps others step up their game.

1 Like