Best practice for conditional icons in RG

I’m rebuilding my apps favourites (likes) system. It was previously working great but used lists, so is not so future proof. Now it simply uses a favourites table with a “user” and “song” field and creates a new entry for each like. If the record already exists it removes it.

I have found 3 methods of showing the conditional heart icons in the RG cells, and would like to know which method is best to use.

Method 1
Each cell in the repeating group has a database search on it that looks for the parent groups song within the favourites table.

Pros: future proof.
Cons: 18 searches for every page load. Wu costs etc.
Graphically a little slow. Hearts are drawn noticeably after the main RG is shown.

Method 2
On page load, all of the users favourites are stored into a state. The RG cells then compare against this state (rather than searching the database per cell).

Pros: fastest UX
Only one initial search needs to be performed throughout users entire session.

Cons: Less future proof, as the entire favourites list must be stored in the state, which could become slow as the list grows too large. (However, we don’t mind limiting the maximum number of favourites, as that has a couple of upsides for our app.)

Method 3
On page load, only the user’s favourites that correspond to the current page of the repeating group are stored in a state. The RG cells then compare their songs against this smaller subset.

Pros: future proof.
Only the relevent data for the RG page is stored.
Only 1 search required per RG page or refresh, not every cell.

Cons: slowest. RG has to load first before it’s page items can be read giving noticeable visual delay.
The search is more complex, having to do “is in” checks.
Search has to be re-performed every time the RG page or data changes.

Which method would the bubble experts here use? Or are there any even better solutions I’ve not considered?

Ideally I would like to use method 1 combined with a temporary state for instant visual feedback…but this depends on how well optimised bubbles searches are when used like this. Does anyone know if bubble would actually charge me for 18 completely individual searches per page/RG refresh, or would it be able to cache & optimise behind the scenes to make this a low cost operation overall? I tried to figure out wu costs by looking into the log but couldn’t get clear answer.

Please also keep in that I may eventually want to offload this job to supabase so making it ‘migration friendly’ is also a consideration.

Thank you :pray:t2:

1 Like

No, never put a search into each cell of a repeating group

That is okay as it likely only incurs the WUs for a single search

As long as you have things setup so you only load the new songs as the user traverses the RG

You’ll get charged for a search for each cell in the RG…if there are 100 songs in the RG, you’ll get charged for 100 searches

Everything is migration friendly prior to the migration since it is impossible to build something for a seamless migration since the migration has not taken place at the time of building.

You could consider storing favorite songs as a single text field that would be a json array of the favorite song Unique IDs…on page load, load the JSON value, then in RG for conditional just reference the JSON value contains current cell’s songs unique ID. You can also store this in local storage so that you do not need to use the database at all.

Interesting question.

I also have a ‘favorites’ on my app where the user can click an icon to save a product as a favorite.

I added the list to the product itself because when the product is deleted, all the stored files are deleted with it instead of having to filter through a separate data type. You may have different needs though for your app.

Anyway, for my setup I have 2 icons…clicked state and non-clicked state.

I use a conditional to show the appropriate icon.

‘when parent group’s product likes doesn’t contain current user this element is visible’

or visible for the other icon etc.

It doesn’t use any extra work units and works fine for my situation.

OK good to know.

Yes this one works well, the only downside is the potentially long delays pushing the results into the state IF the lists got too big. May work ok for my apps needs but wouldn’t work for a twitter(x) clone etc.

Yes I got this one working and it was ‘almost’ ideal, just too visually laggy due to the required order of operations. This one would be the best solution for massive lists.

The only issue with this, I think, is that it becomes a bit clunky gathering stats for the song creators and that’s the main reason i’m moving away from the ‘stored on a list’ approach I am currently using.

Sounds like you’re doing things similar to how I am currently doing them, which works fine and keeps things fast and simple. The only downside is things will likely slow down if the lists grow too big, especially as bubble pulls ALL the data down the pipe when loading a record (unless it’s stored in a satellite dataset of course).

Anyway, thank you so much for the replies so far, I’m going to use Method 2 for now as I think the most likes a user has made so far is around 40, which is manageable. I will switch to a variation of method 3 as and when required.

1 Like

Yes, whatever works best for you.

We did a load test with 10,000 users and it worked fine.

You may have more and the results could be different.

Hopefully, you find the best solution.

How do you do the load test?

1 Like

There are a lot of platforms you can use.

I have a favorite but I won’t advertise them here.

You can google: ‘how do I load test an app?’

Just referencing here a helpful link for list performance info: Better performance: less entries and more lists, or more entries and no lists? - #4 by adamhholmes

I disagree with “never do a search in a repeating group”

there’s actually many times a search within a repeating group is useful and much less WU than alt methods.
the key is to be smart about repeating groups - keep them paged is the main thing to reduce WU

doing an aggregate search is actually very cheap and often cheaper than fetching lists of data and then comparing on the page

that’s likely not the case for favorites but is often the case for total sums since the total is usually on something heavier like “invoice” or “order”

a “favorites” data likely has 50-100 characters so is very light

search for favorites if favorite contains id of the item count >0

the WU cost of that is 0.2 per aggregate search
so 10 rows in a repeating group is 2 WU

VS

returning a list of 100 favorites to the page then comparing each RG cell to the list of favorites

well that’s 1 database search 0.3 wu, plus 0.015 per record, plus 0.000003 wu per character (included bubble id)

so lets say 100 favorites 0.3 + 0.015x10 = 0.15wu + 0.06wu (100x100 characters x character cost) so 0.51wu

but if you had 1000 favorites then its 5wu

wu cost really depends on how you use it

for instance you might have the user click the icon to add and remove favorites and they click the icon multiple times - well each click is a database write so that’s 0.5wu per click

but if you store it as a state and then only save it at the end of them making their selections you could turn 20 database writes or 10wu into 0,.5 wu by leveraging states on the page

the best example of this is a popup
you have a popup with 10 fields - if they are autobind then each field change is 0.5wu
if you instead put a save button then only the save button is 0.5 wu (if all the fields are written in 1 database write step)

you must also be careful with “user” data since "user is loaded on every page load
so you want to keep the “user” lightweight as possible - ie don’t store the favorites on the user since you’d be paying for all the characters in the ids of the favorites list

normally I’d do
user
user favorite

and for ultimate simplicity of the data structure I use a text field to hold the id of the thing instead of a linked thing field - that way I can map an id of any data type into the field and it’s the same cost in WU but my database structure is much cleaner

delete also costs 0.1 wu so it’s more efficient to delete than to keep the favorite and just change a boolean just FYI

1 Like

Yes, the WU cost of the one search returning 100 items is less than an individual data request in each cell…so 0.51WUs is less than 2WUs, which makes it a better approach than using a search in each rg cell.

Additionally, you can make it so your search of favorites will be restricted by the unique IDs are in the RG list of things unique ids list, which will restrict the number of results returned so as to only return those that are currently displayed in the RG, so you wouldn’t ever be in a hypothetical situation of the search returning 1000 items, the RG showing only 10, instead it would be search returns only 10 items or less and RG shows 10 items.

It’s okay to disagree with best practices…maybe the statement would be more easily agreed as best practice if stated ‘at all costs avoid searches within RG’.

This requires you do a search for the thing everytime you want to retrieve it instead of just referencing the linked fields thing (I believe it is not a WU issue as the costs I believe are the same at 0.02 as an individual data request)…it just makes it more difficult to access the values in different parts of the app as a search will always be required. And the sense that the data structure is cleaner if the field is text instead of a related data type is not accurate in my opinion as both are the same thing, only difference is the related field shows as the data type name instead of text…both are just as ‘clean or simple’ as the other in terms of structure.

shipment_id is text and shipment_related is the related field…looks just as clean and simple to me…why do I have both? Storing the ID alone makes it easier for me to get the ID of shipment as I don’t need to load the entire shipment data for when I need to use the ID only, like in an API call, but I have the related field as well to make it easier when I need to access the entire shipment data fields.

The additional costs of characters returned is negligible as it offsets the WU costs for other areas, like accessing the ID only or all fields of the related data type.

1 Like

yes having a search per cell is fine if it’s only 10 cells which do not change often. However in our app for example there are 18 cells per page and over 40 pages of products, and this is going to continue to grow. So those WU costs would really stack up when users are browsing from page to page, or adjusting filters.

The debouncing/multiwrite tip is a good one, and is what I was using previously… a timer would (re)start when a user clicks a favourite and not write anything to the database until it had detected the user had stopped clicking (ie check that a number of seconds had passed without any new favourites being added, then write). This worked good with my list approach as could use “set list” + 1 write to store the data and no UI pauses or delays.

With the new method of storing the favourites (individual items into a database table) this debouncing system is not possible/easy because bubble can not easily ‘create a list of things’ on the front end without using a looping plugin, and it adds UI delays. Technically it could be done on the backend but that then starts getting more expensive. So for now, there is no debouncing (other than to ignore multiple mouse clicks) but it’s something i’d like to implement next.

Interesting stuff.

I use text to hold the id so I can map more than one data types id into the same field else you’d have a data structure like

shipment
carrier
company
contact
etc

for data like a favorite which you may want to use to favorite many different things in your app.

if you use a text field then the data structure is simply

id of thing favorited (which can now be any id of any data type)

I agree that the best practice is to never do a search within a repeating group - but I disagree with “you should never do a search within a repeating group”

there are a few times when it makes sense to do searches in repeating groups as it uses less WU.

How to you handle the need to know which id is for what data type? If you put unique ids of different data types into one field, how do you differentiate what id is for what data type?

for something like favorites or history you don’t need to know the data type - you can just do a search of things where text is this things unique id.

if you needed to know the data type I guess you could have an option set or a text field to map that data… it’d still be cleaner than have a “history” data with 50 fields to link all the other data types to it (for example).