Is Window.size an anti-pattern?

TL;DR Remove all functions in Window and replace with a single subscription.

When writing JavaScript I’m usually lazy and just use window.innerWidth somewhere, thinking that “People never resize their windows, right? So I won’t bother hooking up some window resize listener…” Then a couple of weeks later this comes back and bites me, and I end up implementing the window resizing stuff after all. Every single time.

The Window package basically has two functions: size : Task x Size and resizes : (Size -> msg) -> Sub msg.

The way I always end up using Window in Elm is returning Task.perform WindowSize Window.size in init, and Window.resizes WindowSize in subscriptions.

Now to the thing I wanted to discuss.

Why do we even use Window? Because some calculation depends on the window width. Not the window width at startup. The window width at any given time.

So why not exposing just a single function, size : (Size -> msg) -> Sub msg, that produces a value immediately as well as when the window resizes?

  • No need to set up both an initial Cmd and a subscription. Easier to get going! Also no need for beginners to look up Task.perform.
  • Impossible to make the mistake of only measuring at startup.

Thoughts?

2 Likes

This seems like a good idea to me, though admittedly I only have one data point to contribute. I’m definitely doing exactly this two-step dance in my init function here and my subscriptions function here. The code savings (in terms of lines of code) is minor, but I think the conceptual simplification is worthwhile.

2 Likes

I don’t bother with Window.size. The mental model that I have for the init function is that it returns two values: the information I need to start the app (the Model), and the information I request when I start the app but don’t need immediately (the Cmd). The window size falls in the former in my mental model, so I don’t request it. The solution I have arrived to is to pass it as a flag, alongside other information I need immediately like: today’s date, tokens, API URLs, random Int for Generators, etc.

3 Likes

Doing the same for my projects.

1 Like

@cjduncana and @mattpiz could you share your use cases where you only need the of size the window happens to be when the Elm app boots? My point was that this is an anti-pattern.

Flags, init Cmds or subscriptions – in all cases you need to put the window size in the model, so why not use the subscription and get updates for free?

Layout decisions like the presence of parts, or column numbers in my app is determined by the size of the window, so I have to store this information in the model. I use style-elements in my project, and they offer the Device type to help me make layout decisions. To initialize Device, I need to give it the window’s dimensions.

I do use Window.resizes. As the argument, I pass it the composition of Element.classifyDeviceand my message constructor. This is great to update layout when the dimensions change, but it does not help me initialize the window size in my model, which is the same problem I have Window.size, so I use flags to solve that particular problem.

Ah, sorry, I confused Window.size and Window.resizes. So you do subscribe to window size changes.

Using flags instead of Window.size is pretty clever! I usually initialize the window size in the model to 0 and have Window.size immediately update it, but I guess using flags can be easier.

I could’ve done that, but since zero is not the real representation of the window’s dimension, I avoided it.

My use case is exactly the same as this one. I have a Device type that gets updated each time the window resizes. It is initialized with flags at the beginning. Then responsive layout is entirely done using style-elements depending on this Device. Sorry @lydell if I wasn’t too verbose previously. If you are interested in the corresponding code, the app in question is this demo app. And the code is on github.

One thing you will notice if you go check the code, is that instead of window.innerWidth, I’m using document.documentElement.clientWidth because I’m interested in layout viewport, not visual viewport. So I do not use Window.resizes but my own port (in index.html), but that’s just implementation detail.

1 Like

Really neat app Matt! I was drawing some shapes on it and I’ll definitely be checking the code out :slight_smile:

Since this thread has sprung back to life…

Yes, it is annoying to need both a command and a subscription to do this. More annoying for the pedantically inclined is that the specification says nothing about the order in which messages will be delivered. What if the window is in the midst of resizing when the app is initializing. Is there a chance that the command result will arrive in some sequence with the subscription results that results in the wrong final value being delivered? Reportedly no, but the documentation is silent.

As for why one needs both, the issue is that issuing a command is a concrete event at the end of an update cycle and as such can trigger immediate activity. Subscriptions are just a measure of interest in a particular event and once functions to translate the underlying data become involved cannot be readily compared to one another to tell whether the subscription being returned now is a new subscription or is simply the current instance of an existing subscription. For example, if one were to write (this is contrived):

subscriptions model =
    Window.resizes (WindowResized model.resizeCount)

then each time the runtime calls subscriptions it will build a new tagger function which will make the subscription look different from the previous one. In fact, it will be different whenever resizeCount changes. So, should it then deliver a result or not? Now, as I said, this is contrived in that one should probably wait for the update code to look at the model properties but the point is that it becomes impossible in practice to tell when two subscriptions are the same which is a pre-requisite to being able to fire an event when a subscription is created.

This is actually an interesting problem, not for window sizing where the command plus subscriptions is a bit awkward but relatively straightforward, but for what one would do if one wanted a way to cancel tasks. The HTTP progress module demonstrates that subscriptions can be used to provide cancelation for HTTP requests and as such could provide a pattern for canceling tasks or at least task chain execution on loss of interest. But note that it requires clients to supply unique identifiers for the subscriptions to address the issues above and these unique identifiers are themselves potentially awkward to generate unless one has natural identifiers or a readily accessible centralized source for such identifiers. The subscription handling code also has to decide what to do in the event that the other parameters to the subscription change without the identifier changing. What one almost wants is a command to create a subscription since the command execution essentially provides identity to the subscription.

Mark

I completely forgot that you can turn subscriptions on and off based on the model.

So what you’re saying is, if this fires straight away:

subscriptions model =
    Window.size WindowSize

… then should this one too?

subscriptions model =
    if model.oneSecondPassedSinceStartup then
        Window.size WindowSize
    else
        Sub.none

… and what if the msg changes?

subscriptions model =
    Window.size
        if model.oneSecondPassedSinceStartup then
            WindowSize
        else
            always NoOp

Yes, I guess that makes the whole proposal fall. Which is sad because I doubt anyone uses anything other than a single window size subscription that never changes.

Perhaps all that should be done is adding a tip in the docs about how easy it is to subscribe to the current window size in Elm. And perhaps get rid of Window.width and Window.height.

Or perhaps everything is fine the way it is and we’ve all learned a bit more about subscriptions.