paint-brush
The importance of state ownership in Elmby@julianjelfs_61852
1,043 reads
1,043 reads

The importance of state ownership in Elm

by Boolean Julian JәlfsMay 31st, 2017
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Disclosure — I don’t have all the answers to the problems I am posing here — as ever, there is more than one way!

Company Mentioned

Mention Thumbnail
featured image - The importance of state ownership in Elm
Boolean Julian Jәlfs HackerNoon profile picture

Disclosure — I don’t have all the answers to the problems I am posing here — as ever, there is more than one way!

Elm manages state using a the single state atom pattern that will be familiar to users of React with redux. The state is initialised and then “modified” in the update function as actions occur. The state is typically passed into the view function to be used in building the UI.

So far so good, but The Elm Architecture is also fractal in nature. This means that it is typical to compose multiple instance of this patterns together in a tree. This usually means that our state becomes hierarchical too.

For example in a typical SPA we might have a structure like this:




RootComponentPageOnePageTwoPageThree

This represents the whole App and then components for each page (related to each route in the app). In this situation it is typical to create a top level Model that looks something like this:





type alias Root ={ pageOne: PageOne.Model, pageTwo: PageTwo.Model, pageThree: PageThree.Model}

And this structure might be initialiased like this:






initialiase: Modelinitialise ={ pageOne = PageOne.initialise, pageTwo = PageTwo.initialise, pageThree = PageThree.initialise}

This is a really useful pattern because it enables us to hide the structure and initialisation logic of the page level Models within the page components.

But who owns this data? And what does that mean? I will define ownership as follows: a component owns data, if that data is updated solely in the components update function. So the RootComponent’s update function might look like this:











update: Msg -> Model -> (Model, Cmd Msg)update msg model =case msg ofPageOneMsg sub ->let(subModel, subCmd) =PageOne.update sub model.pageOnein( { model | pageOne = subModel }, Cmd.map PageOneMsg subCmd )...

We recognise this as pure boilerplate i.e. we are simply delegating to the PageOne component’s update function and we know that it is responsible for modifying its model in response to the action.

But what if it’s not that simple (and it never is)? Let’s say that we have the concept of a user and this is generally a root level concern. This may change our RootComponent’s Model to something like this.






type alias Root ={ user: User, pageOne: PageOne.Model, pageTwo: PageTwo.Model, pageThree: PageThree.Model}

And it would be quite typical for us to want to access information about the user at all levels of the app. So how do we achieve this? PageOne’s view function is passed a PageOne.Model so it does not have access to the RootComponent.Model’s user property. Our natural inclination is to push the user property into the PageOne.Model as well.






initialiase: User -> Modelinitialise user ={ pageOne = PageOne.initialise user, pageTwo = PageTwo.initialise user, pageThree = PageThree.initialise user}

This is fine as long as the user never changes. If there are events in the app’s lifecycle that can change that data then you have created a problem for yourself that you might not have noticed yet.

The problem is one of ownership. You now have two versions of the truth. RootComponent’s Model and PageOne’s Model. Let’s assume that our user signs out. Who handles this type of action? Most likely the RootComponent:







update: Msg -> Model -> (Model, Cmd Msg)update msg model =case msg ofSignOut ->( { model | user = signOut model.user }, Cmd.none )...

So we would say in this case that the RootComponent owns the user state. The problem with the code above is that it does not update the copy of the user that we also added to the PageOne Model. We have two source of the truth and they are out of sync all of sudden. Hopefully we at least notice.

Assuming we do notice, what do we do?

Synchronise the data?

What we seem to be saying is that when the SignOut action occurs in RootComponent we need to update the model of certain child components as well. One option is to simply do that in the update function of the RootComponent.







update : Msg -> Model -> ( Model, Cmd Msg )update msg model =case msg ofSignOut ->letpageOne =model.pageOne

            signedOut =  
                signOut model.user  
        in  
            ( { model  
                | user = signedOut  
                , pageOne = { pageOne | user = signedOut }  
              }  
            , Cmd.none  
            )

What’s wrong with that? A couple of things. Firstly, we have broken our encapsulation. The RootComponent now needs to know the structure of the PageOne Model. This makes the PageOne Model more difficult to change. Secondly, it’s all too easy to forget to do it. It won’t be immediately obvious that we have a problem.

We can perhaps fix the first problem by delegating how to update PageOne’s model to a PageOne utility function.














update : Msg -> Model -> ( Model, Cmd Msg )update msg model =case msg ofSignOut ->letsignedOut =signOut model.userin( { model| user = signedOut, pageOne = PageOne.updateUser signedOut}, Cmd.none)

This is better. We have regained encapsulation. But the more serious problem remains. There is nothing stopping us from forgetting to do this.

That function PageOne.updateUser looks a lot like a specialised form of a normal update function doesn’t it? Would it be more palatable if we were just calling the normal update function?







update : Msg -> Model -> ( Model, Cmd Msg )update msg model =case msg ofSignOut ->letsignedOut =signOut model.user

            ( pageOne, pageOneCmd ) =  
                PageOne.update   
                    (PageOneMsg PageOne.signOut) model.pageOne  
        in  
            ( { model  
                | user = signedOut  
                , pageOne = pageOne  
              }  
            , Cmd.map PageOneMsg pageOneCmd  
            )

You might prefer this, but really it’s the same at the end of the day. I still cannot think of a way to make this cascade of updates automatic (or at least enforced by the compiler).

A step in the right direction?

Maybe we can add the user to the child component’s models and just have a convention that it must be replaced with the parent’s copy on each call to update / view. So RootComponent’s update function might look something like this:











update: Msg -> Model -> (Model, Cmd Msg)update msg model =case msg ofPageOneMsg sub ->let(subModel, subCmd) =PageOne.update sub model.pageOnein( { model | pageOne = updateUser model.user subModel, Cmd.map PageOneMsg subCmd )...




-- a general util that can be used from anywhereupdateUser : User -> { a | user: User} -> { a | user: User}updateUser user model ={ model | user = user }

Now we are not going to get out of sync but only if everyone remembers that this is the pattern to use. With this approach we will continue to get caught out from time to time when we forget.

Do not allow two copies of volatile data

We must conclude that we simply cannot allow there to be two copies of any volatile state. But this takes us back to the problem, how does the PageOne component access the user. The answer is to ensure that any such state is passed in from the parent component along with the child component’s model, but not as part of if. So PageOne’s update signature might end up looking like:

update: Msg -> User -> Model -> (Model, Cmd Msg)

And if we needed this state in the view we would want to make a similar adjustment to the view signature. Notice that this is quite similar to the approach taken in React where we have props (which is data passed from above that I do not own) and state which is the data that I am in control of myself.

The big advantage of this approach is that we cannot fail to pass it in from above every time and we do not have two copies of the data any more. Nothing can get out of sync which is very valuable. The downside is that you have to be careful if you are going to make this scale. What happens when I discover another piece of state that is really owned by an ancestor component? I have to add that to the signature as well. All the way down.

It can add to feeling that there is already a bit too much boilerplate for comfort. But in my view, it is the only real solution. Please let me know if you think there is another option!

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising & sponsorship opportunities.

To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.

If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!