Managing global state in Elm

2,021 views
Skip to first unread message

James Wilson

unread,
May 31, 2016, 12:29:44 PM5/31/16
to Elm Discuss
In Elm, each component basically has its own internal state (which is actually all just a slice of one global model). In my app, I also want global state that is independant of any components; for example a clientside cache of various API responses (asset details - there could be many thousands, user authentication status).

I want any component to be able to call methods that make use of this global state. For example, a method to obtain details for items in the current view might first look at the global state to see if these items are cached. If they arent, the call would provide a Cmd to be issued that gets the items (and puts them in the cache), while simultaneously updating the state to indicate that they are being loaded (so that the same request again from another component doesnt trigger another call to the backend). If they are cached, they can be easily returned from there. A first shot at a signature might look something like:

getItem : GlobalState -> ID -> Tag -> (GlobalState, Cmd msg)



However we could partially apply functions that exist on some globalState instantiation to hdie the initial state being passed in and end up with:

state.items.getItem : ID -> Tag -> (GlobalState, Cmd msg)



The downside of this approach is that I have to thread this state through multiple calls that might make use of it, and thread it back up explicitly through the update functions to get it back to the top. At the top we'd then have something like (excuse any mistakes!):

update msg model = case msg of
   
SubMsg m ->
     let
(newSubModel, subCmds, newGlobalState) = SubComponent.update m model.subModel
     
in ({ model | state = newGlobalState, subModel = newSubModel}, Sub.map SubMsg subCmds)
   
...


An alternative approach is to hold this global state in an effect manager, and so in the app you'd end up using the Cmd/Sub mechanism to ask for things from the state and internally initiate API requests to update the state as necessary. We'd end up with an API more like:

getItem : ID -> Tag -> Cmd msg


or

state.items.getItem : ID -> Tag -> Cmd msg


where the returned Cmd would either lead to an item being sent to the component immediately via a cache (where Tag is a Msg type the component knows about) or after it was obtained via some backend. This would make all retrieving of state async but seems to simplify the interface (perhaps at the cost of more complexity in implementing the effect manager).

Which approach do people think is best for working with global state (neither is an option if you have a better way!)? Do you get away with not needing this kind of thing (and if so, how)? I'd love to hear back, especially from those that have had experience building larger apps in Elm!

Simon

unread,
May 31, 2016, 2:11:16 PM5/31/16
to Elm Discuss
This is an interesting question and important for larger apps. 

In the instance I am working on I plan to use a cache, and have implemented the idea in one of the components so far. I have the relevant top level component monitor for events in sub-components triggering messages that require the cache to be supplemented, handling the downloading, and then routing the result to the cache. I pass the cache down through my view functions alongside the relevant section of the overall model, and the view function shows a holding statement for as long as the cache cannot provide content.

 Simon

Peter Damoc

unread,
May 31, 2016, 2:45:42 PM5/31/16
to Elm Discuss
ADT in Elm is one of its most powerful weapons. 

You could encapsulate your requests in a type and use this type at top level to fulfill them. 

For example: instead of returning Cmd msg you return some Req msg that can be turned into a Cmd msg at top level based on some context information. 

Here is a gist with a skeleton of how I view this implemented:
https://gist.github.com/pdamoc/a47090e69b75433efa60fe4f70e6a06a

I've sent the base of the URL as a simple String in `Req.toCmd` but you can imagine a more complex type holding all kind of information (e.g. cache, auth, etc ) . 
Also, I've kept the type of the Req simple (only saved the rest of the URL based on the user and the request) but one could use it to store all the info needed when you will turn the Req into a Cmd. 






--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
There is NO FATE, we are the creators.
blog: http://damoc.ro/

James Wilson

unread,
May 31, 2016, 3:05:03 PM5/31/16
to Elm Discuss
The key part that's not coded in the gist is the use of a cache/global state object, however I think I  see what you're getting at - pass back up the chain a Req object, say, and at the top we can turn it into a Cmd using, say, some top level global state as well as whatever other data we need. This may lead to a request being made to the server or it may not.

The other part of the puzzle is actually updating the cache when a request is made. Req.toCmd for instance could return an updated GlobalState so that it's able to cache "pending" states on values (so that we can avoid duplicating requests). To update the cache when the response actually comes in we could have toCmd return a Cmd.batch of 2 commands, one that will fail/succeed and send a message to the component that initiated the Req, and one that will send a message aimed at the top level cache itself.

Thanks Peter, I'll definitely mull over this!

Peter Damoc

unread,
May 31, 2016, 3:43:41 PM5/31/16
to Elm Discuss
The updating of the cache sounds to me like this: 

1. if we have the info in cache, just supply the info without a HTTP GET
2. if we don't have the info in cache, return a different Msg that encapsulates the msg that requested the original information and the info required for the cache update. 

Here is a quick update of the code I've previously posted to include this caching mechanism. 

https://gist.github.com/pdamoc/d492ab58023926cd4d4950f12e5e170d



James Wilson

unread,
Jun 1, 2016, 3:21:21 AM6/1/16
to Elm Discuss
Thanks, that looks like basically exactly what I'd have guessed :) It's super useful seeing an actual code sample with these ideas in.

One thing I wonder now is; why not use an effect manager for this? It basically seems to fit the exact same space (allows you to create a custom Req like thing that can be mapped and batched and passed up the component hierarchy - except that it's just a Cmd instead and plays nice with other Cmds; allows you to maintain and update state (the cache) as you go; allows you to "convert" Reqs to tasks to be run - just Cmds again now). In fact, effect managers don't really seem to help you do anything other than what's described here (plus a subscription side if you want it). Are there any cons to using an effect manager here that you have in mind?

Peter Damoc

unread,
Jun 1, 2016, 3:31:11 AM6/1/16
to Elm Discuss
I haven't used an effect manager for this because I haven put in the time needed to learn how to create effect managers. :) 

If what I've shown here can be accomplished with an effect manager then that's the way it should be done.  :) 





James Wilson

unread,
Jun 1, 2016, 3:45:05 AM6/1/16
to Elm Discuss
Thanks for your help; I'll have a ponder and probably end up taking an effect manager route again (I started this way and then ended up with some weird hybrid where my cache was in the main elm app and an effect manager did the "lower level" api stuff; but I'm not so happy with it)

As another effect manager code point if you're intrigued how they work (and my comments are in any way useful, which they may not be); here's an effect manager I made for sending and receiving things from a backend I'm wiring my app up to (so it uses custom Cmds and Subs):


Not sure whether it's any help (may just be easier looking at the elm-lang effect managers!) but just in case :)

Simon

unread,
Jun 1, 2016, 3:18:41 PM6/1/16
to Elm Discuss

This is super cool.
But what is the function:

(!) : a -> List b -> (a, b)

Peter Damoc

unread,
Jun 1, 2016, 3:42:16 PM6/1/16
to Elm Discuss
I'm guessing you referring to: 

http://package.elm-lang.org/packages/elm-lang/core/4.0.1/Platform-Cmd#!

This is the code : 

(!) : model -> List (Cmd msg) -> (model, Cmd msg)
(!) model commands =
  (model, batch commands)


I use it as a short hand for joining the model and the Cmds.
It's also pretty handy when you don't have a Cmd since `batch []`  evaluates  to Cmd.none. 




Tim Stewart

unread,
Jun 1, 2016, 8:52:16 PM6/1/16
to Elm Discuss
This is a bit pedantic, but over the course of learning Elm I have sometimes found the use of magic symbols a bit confusing. This is the first I heard of the ! operator. In 2 of the three places it is used in the code [1] the ! infix operator is not really shorthand for the tuple form because it amounts to the same number of characters (surrounding a single Cmd with square brackets instead of the entire expression with round brackets, and using an exclamation mark instead of a comma), and it is only a shorthand in the other place because the empty list evaluates to Cmd.none. The tuple form would have been more familiar and readable to a relatively inexperienced Elm programmer like me - particularly in the place where it added most conciseness, as Cmd.none is surely more expressive than [].

Kudos for sharing the code example. I don't mean to be critical, just reflective.

Peter Damoc

unread,
Jun 2, 2016, 1:32:51 AM6/2/16
to Elm Discuss
Tim, thank you for pointing this out. 

In the interest of clarity I would like to add that ! is not an operator but a function. 

Elm allows one to define single character functions and if the function name is made of symbols, it can be used inline without backtick marks (you have to declare the name inside parentheses). 

! first appeared in 0.17 so it makes sense to be new for a lot of people. 
I also can see how this is something that might require some getting used to. 

I used to have a function in my code called "noFx" that I would use to avoid typing a lot of Effects.none in update but I find this infix function to be much nicer. 
Even in the case of returning a single Cmd (where I'm not saving any characters) I found myself preferring this form for uniformity reasons.

Anyway, thank you again for bringing this to my attention. 
I now see that Evan's examples (where I first encountered this pattern) moved to a more explicit (model, Cmd.none). 
I'll try to do the same. :) 

 


suttlecommakevin

unread,
Aug 23, 2016, 11:11:53 AM8/23/16
to Elm Discuss
Great question, OP. I've been wondering about the same thing. React has pretty clear "best practices" when it comes to component vs global state, but it's also not immutable by default

Does Elm have best practices for managing and reconciling state between components and global application state managers?

Erik Lott

unread,
Aug 23, 2016, 3:06:53 PM8/23/16
to Elm Discuss
Does Elm have best practices for managing and reconciling state between components and global application state managers?

Yes: don't hold global state in your components' models :)

It might help to think about this question differently. Going back to the OP's original question, let's forget about the caching mechanism for a moment, and imagine that all of the "global state" is already loaded into the app (via flags). and that state is stored in the top level model of the app.

The first problem becomes: "I want my components to be able to make use of (display) the global state". There is already have a good answer for this. Have a look at the sortable table example for the answer - any global state/business data that needs to be displayed in a component will be provided to the component's view method from outside of the component. For the OP, this has the added benefit that components no longer need to make http requests to load global state, since the global state is provided from outside of the component.

The second problem becomes: how do we load this global state/business data into the application and cache it? This requirement is now fairly easy to wire-up in elm since the http requests and data cache all live at the same level of the application (the top level). If anyone wants more details on how to do this, let me know. 

I hope that helps

Birowsky

unread,
Nov 30, 2016, 12:04:08 PM11/30/16
to Elm Discuss
Hey Eric, please elaborate on this one. How do you envision leaf component triggering global state update?

Erik Lott

unread,
Dec 2, 2016, 8:45:12 AM12/2/16
to Elm Discuss
Birowsky, your leaf component will need return messages to the parent, and the parent can act on those messages to update it's state. There's no magic to it unfortunately. Do you have a specific problem you're trying to solve?

Birowsky

unread,
Dec 2, 2016, 9:25:48 AM12/2/16
to elm-d...@googlegroups.com
The problem i see with this approach is that in hierarchy of deeply nested components, the whole ancestry would need to know about the intention of the leaf. I was hoping towards more of a command-like approach.


--
You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/j-Wa6NGiYUM/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.

Erik Lott

unread,
Dec 2, 2016, 10:23:01 AM12/2/16
to Elm Discuss
The problem i see with this approach is that in hierarchy of deeply nested components, the whole ancestry would need to know about the intention of the leaf.

Sort of. Each module only needs to consider it's direct child/children. So, to use a module, you have to use it's API.

I understand your concern, from a theoretical standpoint, but having a large complex elm spa ourselves, this just isn't an issue. Depending on how you choose to build your application (I assume you're building an SPA), it is very likely that every main architectural module (page modules) will have a nicely developed API that allows the enclosing module (main module) to "hook-in" to events that are happening internally to it. It's actually very pleasant to manage once you get used to it.

For example, here is an imaginary module:

MyModule.elm

type Model =
  Model {..}


type alias Config msg = 
  { onClick : msg }

view : Config msg -> Model -> Html msg
view config model =
  button [onClick config.onClick] [text "Submit"]

The view function allows parent modules to configure the view to return parent messages. Here is an example of a parent hooking into the view function

Parent.elm

type alias Model =
  { myModuleModel : MyModule.Model }

type Msg 
  = ChildClicked

view : Model -> Html Msg
view model =
  MyModule.view myModuleConfig model.myModuleModel


myModuleConfig : MyModule.Config Msg 
myModuleConfig =
  {onClick -> ChildClicked}

Forgive any mistakes - I'm coding this quickly. So, that is just a tiny example. You can do the same thing with your update functions (passing in an update config) but the arrangement is a little different. Does that help?

Mark Hamburg

unread,
Dec 2, 2016, 10:54:02 AM12/2/16
to elm-d...@googlegroups.com
To use effects managers or not to use effects managers. The Elm guide basically says "don't" and then proceeds to not provide much documentation about how to write an effects manager if you wanted to. I guess one could take that as a hint.

TL;DR You don't need to write effects managers but you will end up writing more (and more explicit) code if you don't.

If you replace commands with the list of out messages pattern discussed elsewhere on this list — an internal update function doesn't return a command but rather a list of out messages to be processed by the next level up (where processing can include simply forwarding) — then you can, I believe, do everything that a command-only effects manager can do. At the top level of your app, you recognize the out messages destined for the pseudo-effects-manager, collect them up, and deliver them to an update function for that pseudo-effects-manager. When the pseudo-effects-manager has responses, it delivers them via out messages that the top level can relay down to the appropriate component.

This does tend to result in tagger functions getting stored in the pseudo-effects-manager portion of the model. Effects manager store functions in their state and we would expect the same for pseudo-effects-managers.

This does result in more plumbing code than effects managers. That's because effects managers are taking advantage of special routing support in the language and runtime and this approach requires building it all oneself. That said, one of the benefits often cited for functional programming and the justification for things like threading context information through view functions instead of relying on mutable globally visible state is that it is both more clear and more explicit. So, if magic bothers you and you prefer to be explicit, this approach should be preferable to the effects manager approach.

All that said, what about subscriptions? Here we have more of a problem. The subscriptions function cannot mutate the model so it can't be built by flowing up an internal subscription equivalent and handing those to our pseudo-effects-manager to decide what to do because the pseudo-effects-manager can't change state either. A real effects manager, existing outside of the model, can and generally does update in response to the subscriptions. Effects managers enable one to have state outside of one's model that can mutate even when one's model cannot.

So, what could we do? If we are willing to update pseudo-subscriptions on every update — and as I understand it, that's what the Elm runtime does with real subscriptions — then we could certainly do this at the end of our top level update functions. In other words, subscriptions are feasible but maybe a bit less pretty in how the logic flows.

Mark

Birowsky

unread,
Dec 12, 2016, 5:01:35 PM12/12/16
to Elm Discuss
Erik, Mark, thanx a ton for your responses!

Erik, I was using that approach solely for the views but you made me think how I might do it in the updates too, which from here, seems like quite the prettification.  Thanx for that!

Mark, wow, thanx for the effort. If you could believe it, I built the whole thing. It was uber complicated considering it's one of the few things i've done in elm. There is so much plumbing and routing, in order to add a single api endpoint, i need to touch the codebase in exactly 8 places :}

I still don't whine tho. The good parts of Elm outshine any crap. I'm off to my next challenges. 

Have fun, communitymates!
Reply all
Reply to author
Forward
0 new messages