Fastest, most lightweight, pseudo-random generator (for HTML IDs)?

A lot of times when I write functional components and live components, I want to make sure I don’t end up with duplicate HTML IDs so I do something like

<div id={"my-component-#{System.unique_integer()}"}

or

<div id={"my-component-#{Ecto.UUID.generate()}"}

or

<div id={"my-component-#{:rand.bytes(10) |> Base.encode16()}"}

I think the last one is best because it is “random” instead of “unique”. Since I am only trying to safeguard against duplicate HTML IDs in the same page, using a “unique” generating function feels excessive.

The question: how do you avoid duplicate HTML IDs and what is a well-suited function to use for doing it?

I would gladly welcome any advice on this.

2 Likes

:wave:

# produces something like "-576460752302382141"
iex> :timer.tc fn -> Enum.each(1..1000, fn _ -> System.unique_integer() |> Integer.to_string end) end
{746, :ok}

# produces something like "FE404" <- I like this one
# note: grows larger and larger the longer VM operates, but won't exceed `(NoSchedulers + 1) × (2⁶⁴ - 1)`
iex> :timer.tc fn -> Enum.each(1..1000, fn _ -> System.unique_integer([:positive]) |> Integer.to_string(16) end) end
{787, :ok}

# produces something like "4D37AE7D3E6865A26A64"
iex> :timer.tc fn -> Enum.each(1..1000, fn _ -> :rand.bytes(10) |> Base.encode16() end) end
{1358, :ok}

# produces something like "cd2acfc8-90f2-4c19-9440-5207f49102f2"
iex> :timer.tc fn -> Enum.each(1..1000, fn _ -> Ecto.UUID.generate() end) end
{1722, :ok}

And then there is also Enum.with_index/2 that can be used to give unique ids for a collection of elements.

And I think the latest trend is to actually use fewer ids and instead use aria or data attributes. Summary: You may not need HTML ID's

6 Likes

Why would it be excessive? All that System.unique_integer/0,1 promises is that it will not return the same value twice for the same options, so it’s actually a very cheap function, it essentially just bumps a thread-local counter and returns it.

5 Likes

Liveview does require idd for a few things, for example when you use hooks.

3 Likes

If you use System.unique_integer() how do you know what the ID is? And if you don’t know what the ID is how is it being used, like why is it there?

2 Likes

Exactly. I use meaningful strings for IDs and collect all IDs strings into one module as macros to avoid name collision.

The requirement comes from LV. It tracks dom node livecycles via ids (so that a new id means a new livecycle) e.g. for hooks as well as for streams. Sometimes being unique is enough of a property to make those things work, even if there’s no further targeting of those ids. It’s enough that they happen to be different.

2 Likes

When using a JS hook.

this.el.id

1 Like

Don’t LV ids need to be deterministic? I’ve been warned not to use any kind of non-deterministic generation of ids on here before.

Enum.with_index is a good solution. There is also %Form{}.index. Otherwise if there is a schema involved I just schema_name_#{@schema.id}.

1 Like

I’d still recommend it be something the caller sets and is meaningful. IE

attr(:id, :string, required: true)
...
<div id={"my-component-#{@id}"}
1 Like

I will have:

defmodule MyAppWeb.Ids do
  defmacro my_component(id) do
    quote do
      "my-component-#{unquote(id)}"
    end
  end
end

and use Ids.my_component(@id) everywhere in the liveview. With another level of indirection, I can avoid potential name collision.

1 Like

I agree that having the ID be stored somewhere is the best solution. It gives you future-proofing.

:wave: @derek-zhou

This might be a stupid question since I’m not very familiar with modern LiveView, but why a macro over a function?


Oops, just realized that it’s off-topic. Sorry!

A function will do; maybe I was thinking too much. A macro will expand in compile time so there is less overhead.

What kind of future proofing does this give you?

This is one of these things where I feel friction with liveview sometimes. I don’t really want to give some things an id, but I have to for liveview. So I’d rather hide it.

To be fair, sometimes you do want an id and then I want to make it visible/explicit.

1 Like

Mostly in the ability to change how you generate IDs later, plus also being able to parse them and infer anything internal encoded inside of them (like a DB ID).

Granted not the most solid of arguments but I had such needs in the past (not with LV) so I came to appreciate having escape hatches.

I swear either Phoenix or Liveview has an internal function for making random uniqueish binary refs in its internals that I’ve stolen for similar purposes before, but I’m having trouble finding it.

Update: it was Phoenix.Tracker in :phoenix_pubsub:

  defp random_ref() do
    binary = <<
      System.system_time(:nanosecond)::64,
      :erlang.phash2({node(), self()})::16,
      :erlang.unique_integer()::16
    >>

    Base.url_encode64(binary)
  end

I borrowed it because I was messing with a custom Tracker-compatible system and wanted same-format ids, but also reasoned if it was good enough for Phoenix it was probably fast enough for my other usecases in the application as well, so I used it for some DOM id generation as well.

2 Likes

@christhekeele Thanks for that! It is a bit slower than this :point_up: one though.

iex [3] > :timer.tc fn -> Enum.each(1..1000, fn _ -> random_ref() end) end
{1627, :ok}
1 Like

So am I just completely wrong about this since no one has acknowledged?

@benwilson512?

What do you mean with deterministic ids?
I’ve never seen anything like that mentioned.