spotifyovercastrssapple-podcasts

Elm Store Pattern

Martin Janiczek joins us to discuss a pattern for declaratively managing loading state for API data across page changes.
June 6, 2022
#58

Transcript

[00:00:00]
Hello, Jeroen.
[00:00:01]
Hello, Dillon.
[00:00:02]
And today we're joined by another guest and indeed another Martin.
[00:00:07]
Martin Janacek is joining us.
[00:00:09]
Martin, welcome and thanks for coming on.
[00:00:11]
Yeah, hello.
[00:00:12]
Glad to be here.
[00:00:13]
Yeah, we're really glad to have you.
[00:00:15]
And today we are talking about the store pattern.
[00:00:18]
You gave a talk recently about the store pattern and definitely worth watching the talk.
[00:00:23]
It's a really fun topic.
[00:00:24]
You want to introduce what is the store pattern?
[00:00:26]
Yes.
[00:00:27]
So the store pattern is basically, I'm just popularizing something that I saw in a codebase
[00:00:38]
that I worked on at a previous company.
[00:00:41]
And I think it's a really nice way to structure things.
[00:00:45]
And I haven't seen it done in other places.
[00:00:49]
So yeah, I thought it would be nice to share some kind of like golden nuggets that we have
[00:00:56]
stumbled upon.
[00:00:58]
And yeah, so it definitely isn't my original idea.
[00:01:03]
And I believe there's even prior art by the company Gizrara.
[00:01:10]
There are some blog posts about a fetch pattern, which I believe is very similar to this.
[00:01:16]
Although maybe we are diverging at some points.
[00:01:21]
But yeah, I think it's a pretty good indicator that we have stumbled upon something nice
[00:01:27]
when two unrelated developers have found a similar pattern.
[00:01:34]
So what it is, is store is just a record of data that you have received from a server,
[00:01:44]
from some kind of HTTP call.
[00:01:47]
So it is mostly a convention where instead of each page having its own requests and holding
[00:01:58]
pieces of the data and passing them to child view modules and so on, the data all lives
[00:02:06]
at the same place.
[00:02:08]
And then instead of using API commands, like HTTP request commands, you have a message
[00:02:23]
type or message like type.
[00:02:25]
You have a custom type where you can say fetch users, fetch posts, fetch details for a single
[00:02:33]
user and so on.
[00:02:35]
And this custom type, I call it the functionalization into, let's say a request type.
[00:02:44]
It unlocks some very nice things for both developer experience and for the user experience.
[00:02:52]
Right.
[00:02:54]
So like for example, if you are on a page listing out users and you click to a page
[00:03:02]
that's the detail view for a user, if there's maybe like there's a certain type of data
[00:03:08]
that is for listing users, there's a certain type of data that is the detail user view
[00:03:14]
which might have additional data.
[00:03:17]
So your store would be like a global type for all of that, like a record with all of
[00:03:22]
the possible data that could be consumed in your entire application.
[00:03:26]
So you'd have a record which would have users is a web data, remote data of a list of users.
[00:03:34]
And then you would have maybe a dict of user ID to web data of user detail.
[00:03:42]
And so you mentioned the developer experience and the user experience.
[00:03:46]
From the user experience side, since you have it as a global store across the application,
[00:03:52]
when you're on the user listing page, you might have a spinner saying I'm loading data
[00:03:57]
of all users.
[00:03:58]
You click to a user, you have a spinner that says I'm loading the data for this specific
[00:04:02]
user's details.
[00:04:04]
When you go back, there's not a loading spinner because you have that data already.
[00:04:09]
Yes, it encourages data reuse.
[00:04:13]
So instead of when you're changing between routes or pages, instead of dropping all the
[00:04:20]
data that could be, let's say, in the model of that page, you have it somewhere top level
[00:04:29]
and it outlives the page.
[00:04:33]
So then you can change between pages and the data stays loaded.
[00:04:38]
And other pages, if they need access to the same data, they don't need to load it themselves.
[00:04:44]
They can just reach into the store and it's already there.
[00:04:47]
Or perhaps they are the first ones to load it.
[00:04:50]
They will load it, but then it's free for all the other pages to read it.
[00:04:56]
Right.
[00:04:57]
And when you say they don't need to load it themselves, they need to declare that they
[00:05:01]
depend on that by saying, if it's not there, I need it to be there.
[00:05:05]
But they do that declaratively.
[00:05:08]
And then the store can figure out whether if it has it, then it doesn't need to do anything.
[00:05:14]
If it doesn't have it, then it says, oh, this page that is loaded depended on that data.
[00:05:19]
I don't have it yet.
[00:05:20]
I need to start fetching it.
[00:05:22]
Yes.
[00:05:23]
So as you said, each page declares what data it needs, but it doesn't really care whether
[00:05:30]
it's loaded already or not.
[00:05:32]
It just says, I need the list of all users and I need the full details of this particular
[00:05:39]
user and the store and the main module handle that behind the scenes.
[00:05:46]
Basically the main module, whenever you change a route, it will run that function.
[00:05:53]
It will decide, okay, we need to run all these requests, but we can skip some of these because
[00:06:02]
they are already loaded.
[00:06:04]
And so this happens whenever route changes or whenever the store changes, because there
[00:06:10]
can be, let's say second order data dependencies.
[00:06:14]
You might load the user and then figure out, oh, there are some other entities related
[00:06:20]
to it that I need to load for this page.
[00:06:22]
So on the first run, the route data dependencies function might tell you, I need just this
[00:06:28]
user.
[00:06:29]
But on the second run, when the user is loaded, it might tell you, I need this user and then
[00:06:35]
I need these three images, let's say.
[00:06:38]
Right.
[00:06:39]
So the way that you declare what dependencies you have is using a, I guess by convention,
[00:06:46]
a data requests function or constant, which is kind of like a subscription, right?
[00:06:53]
You declare based on this model or based on just constant, these are the things that I
[00:06:58]
care about.
[00:06:59]
Yeah, exactly.
[00:07:00]
It looks very similar and feels very similar to the subscriptions function.
[00:07:05]
And in my experience, it doesn't depend so much on the model as on the route itself and
[00:07:14]
on the store, on the contents of the store.
[00:07:17]
But yeah, definitely, I think if you, let's say paginate the list, you might have the
[00:07:25]
current page in the model.
[00:07:26]
And so this function would then depend on the model too.
[00:07:30]
And as you said, sometimes some pages are just so simple that this data requests function
[00:07:39]
is just a constant because it just doesn't depend on anything to decide what it needs.
[00:07:47]
So in a lot of Elm applications you have, or a lot of pages, you have init, you have
[00:07:53]
update, you have view subscriptions.
[00:07:56]
Here you would add data requests.
[00:07:58]
Also sometimes we see these same patterns in non pages.
[00:08:02]
We see them in smaller modules for components in a way.
[00:08:08]
Would you think that this would be applicable as well?
[00:08:11]
Would you have a data request in a sub element that is not a page?
[00:08:17]
Yeah, I believe that would be doable.
[00:08:21]
There's nothing wrong with it, I feel.
[00:08:24]
Although I have tended to just let the page declare all these things and kind of magically
[00:08:31]
know about what the components need.
[00:08:34]
But yeah, it might be a little bit unsafe.
[00:08:37]
Definitely you could have the data requests function also in small components and let
[00:08:43]
them know about the store also.
[00:08:46]
So usually I have just reached into the store from the page and given the components the
[00:08:57]
data that it needs instead of the whole store.
[00:09:01]
But yeah, you could definitely do it.
[00:09:02]
There's nothing wrong with that.
[00:09:04]
Yeah, I mean there definitely is a trade off of convenience of being able to perform commands
[00:09:11]
from somewhere, being able to change the model from somewhere, being able to have implicit
[00:09:16]
state that is sort of sub component style thing has an Elm.
[00:09:20]
But then there's also the traceability and being able to follow where does a change come
[00:09:26]
from, where can a change happen and the wiring of wiring up new things.
[00:09:31]
So in Elm it seems to work pretty naturally when you have more top level sort of controller
[00:09:39]
style things that are, hey, I've got the data you need and I pass it down to more sort of
[00:09:44]
dumb helper functions.
[00:09:47]
That was one of the things I was wondering about is like how do you differentiate between
[00:09:52]
when it's a good idea to create these abstractions that have these sort of Elm style hooks that
[00:10:00]
have like a store has and a knit and an update and a piece of it that stays in the model
[00:10:05]
and it's responding to these sort of actions that you call them, these messages that it
[00:10:11]
can receive.
[00:10:13]
You could imagine going too far with this where you turn all of your logic into these
[00:10:20]
many componentized things that have everything but the view and they all are able to manage
[00:10:26]
their own data.
[00:10:28]
How do you know that you haven't gone too far with those things?
[00:10:32]
How do you decide if it's a good idea to create these abstractions?
[00:10:36]
Yeah, that's a tough balance and I think it's tough to somehow condense into simpler rule.
[00:10:45]
But for the store pattern, it feels like it does a lot of logic.
[00:10:51]
So there is the checking whether something should be loaded or whether we don't need
[00:10:56]
to because it already is or the request is in flight.
[00:11:00]
There are somehow naturally there are these two message like types.
[00:11:06]
There's the data request and then there are the responses and the model is already there
[00:11:14]
by virtue of being this record of the data.
[00:11:17]
So it kind of fell into that let's say component style automatically.
[00:11:25]
So it has all of these, it has update, it has in it, it has a second update let's say
[00:11:32]
for the other message type.
[00:11:35]
So it felt right.
[00:11:38]
I think if you try to apply this pattern a lot, let's say in view components and so on,
[00:11:49]
the pain will tell you.
[00:11:51]
So there is a certain amount of boilerplate that you need to write for all of these.
[00:11:59]
And at least for me, I have written a few of these and then I have written a stateless
[00:12:07]
view component that just gets data and render something and the model of the parent page
[00:12:15]
handles all the state changes and it's just so much nicer.
[00:12:20]
So I think it's a balance that everybody needs to find for themselves.
[00:12:25]
And I remember taking a few parts of a big application and creating those components
[00:12:35]
out of those because they just felt like almost separate pages although they all lived at
[00:12:42]
the same place.
[00:12:43]
And at that point, it felt like I was gaining something.
[00:12:47]
I was gaining some componentization.
[00:12:51]
I put these things into little boxes so there was like detail and list and edit.
[00:13:00]
Although they weren't like pages, there were more like let's say models or just different
[00:13:07]
states of the page.
[00:13:09]
It felt like I was gaining some clarity.
[00:13:12]
So although you can have a lot in one Elm file and it's generally okay to have just
[00:13:18]
like 4,000 lines of code of Elm, sometimes when it feels like you would gain something,
[00:13:28]
just try it.
[00:13:29]
But I wouldn't do it prematurely.
[00:13:32]
I would rather err on the side of having a lot of things at one place instead of having
[00:13:39]
tens of little components that I need to wire into my main page.
[00:13:46]
Try it out but try also to feel the pain of using it and writing it and then come up with
[00:13:52]
a better solution if there is too much pain.
[00:13:56]
In my experience, extracting something around a type and functions for managing that type
[00:14:04]
and encapsulating parts of it so you can have a private part of the interface, there's a
[00:14:10]
pretty low cost to that.
[00:14:12]
But when it's wiring in and it's an update and these things, that's a huge cost and that
[00:14:19]
should be done very sparingly.
[00:14:22]
I do wonder like is it just that the user experience pushes us to this by necessity,
[00:14:29]
right?
[00:14:30]
Like with this store pattern, you're talking about the user experience it enables, not
[00:14:35]
just the developer experience.
[00:14:38]
How would you achieve that same user experience without extracting something like this that
[00:14:43]
has some sense of globally maintaining the state of all possible requests so you can
[00:14:49]
share them across without needing to throw away the data when you go to a new page?
[00:14:54]
Yeah, exactly.
[00:14:55]
I can only guess at what led the original developer to create this pattern in the code
[00:15:02]
that I have first seen it in.
[00:15:04]
But I imagine there was some kind of pain involved and they just solved it by creating
[00:15:10]
this.
[00:15:11]
Yeah.
[00:15:12]
Yeah, I think letting the user experience guide some of these abstractions is a really
[00:15:16]
good idea to say, we don't want a loading spinner when we go back to this page.
[00:15:21]
Maybe that workflow is happening frequently that you're going back to the main index page
[00:15:28]
and it's not updated frequently and you want to manage that.
[00:15:32]
So letting the user experience guide it there, it seems like a very good strategy.
[00:15:36]
So in terms of the user experience, I think that there are many different ways you could
[00:15:41]
go here.
[00:15:42]
And I know that like your experience with it has made certain user experience decisions,
[00:15:48]
but maybe we can dig in a little bit to what kinds of user experiences could this support.
[00:15:54]
I'm thinking in particular of things like stale data, like how to manage stale data,
[00:16:00]
how to manage reloading states, and also how to manage maybe purging parts of data if you
[00:16:08]
send a post request where you're updating something and you know something is going
[00:16:12]
to change and doing like targeted purges or targeted reloads.
[00:16:16]
Right.
[00:16:17]
So for the reloading case or let's say for the stale data, I imagine that this problem
[00:16:26]
would disappear completely with something like GraphQL subscriptions, right?
[00:16:30]
And this store pattern, I have only used it with REST APIs where the client initializes
[00:16:39]
the, where the client requests the data and gets bulk of data back and doesn't get automatic
[00:16:47]
updates, right?
[00:16:49]
What is a GraphQL subscription?
[00:16:51]
It uses WebSockets under the hood.
[00:16:53]
I mean, it can use long polling as well, but it's like GraphQL has the concept of saying
[00:16:58]
you want to select data for a query.
[00:17:00]
You want to select data that represents a mutation.
[00:17:04]
So you say, go and create this user and give me back the data, including I want the new
[00:17:09]
user ID you created.
[00:17:10]
And then there's a third type of thing you can do in GraphQL, which is a subscription,
[00:17:14]
which is like a query, but it's a query that it will keep resending that data you selected
[00:17:19]
like in a query, but it'll send it anytime it changes.
[00:17:23]
So you could use that to build like a real time chat application, things like that.
[00:17:27]
And usually it does it over WebSockets.
[00:17:29]
So yeah, you can have an always up to date list of users because the server is listening
[00:17:35]
and it says, oh, a new user was just added.
[00:17:38]
A new thing was just updated and then resends the data.
[00:17:40]
So it's never, never stale.
[00:17:42]
Yeah.
[00:17:43]
Yeah.
[00:17:44]
So with the applications that I've worked on, it was fine to generally assume that nobody
[00:17:49]
else tempers with your data, but it is very application specific, right?
[00:17:54]
There are different applications with different data access patterns.
[00:18:00]
And so for me, it was fine to just assume I have the data I have is always up to date,
[00:18:09]
but there were some, let's say flow patterns where in one part of the page I created an
[00:18:18]
entity and then I needed to go to a separate page.
[00:18:24]
And if you do that, you do have the newest part of the, you do have the newest version
[00:18:34]
of the data.
[00:18:35]
But we found that there are users that have two tabs open that have two browser tabs open
[00:18:43]
next to each other.
[00:18:44]
And they created the entity on the left one in the left one.
[00:18:50]
And then they were surprised, why doesn't it show up in this other page?
[00:18:54]
Right.
[00:18:55]
I just created it.
[00:18:56]
So we, at the time, instead of doing some kind of tab to tab communication, instead
[00:19:05]
we just gave them a refresh button.
[00:19:08]
And so they learned that they can click the refresh button.
[00:19:14]
It would basically throw away the old data.
[00:19:17]
It would load it again and they were happy.
[00:19:21]
So it was kind of a low cost solution with some manual user effort, but it did the job.
[00:19:32]
So with the store pattern, you can, you still have all the control over your data, right?
[00:19:38]
It doesn't lock you out of working with the record, working with the dictionary and so
[00:19:44]
on.
[00:19:45]
It is just a common place to put things in.
[00:19:48]
It is commonplace to look at, okay, if I need the data, it will likely be in the store.
[00:19:56]
So when you say you throw it out, the data to refresh it, you actually set it to an empty
[00:20:04]
list or you remove the data from the store and then the data requests kick in again automatically.
[00:20:11]
No, sorry.
[00:20:12]
No.
[00:20:13]
Okay, cool.
[00:20:14]
So instead, I believe we didn't really clear the data.
[00:20:20]
We didn't override it with not asked, let's say the not asked constructor.
[00:20:26]
Although that would have worked.
[00:20:28]
It would work.
[00:20:29]
It would kick in automatically, but we just sent the request again without looking at
[00:20:36]
whether the data is already loaded.
[00:20:38]
And then the message that deals with the successful response, it also doesn't check whether the
[00:20:46]
data is already there.
[00:20:47]
So it just overrides it on its own.
[00:20:50]
So the user saw the data and they didn't see a spinner that might be possible improvement
[00:21:00]
where we would have a different remote data constructor that has loading, but also the
[00:21:05]
data we could actually reload.
[00:21:08]
Yes.
[00:21:09]
But what we did was just have the refresh button send the command again or send a special
[00:21:17]
command that doesn't check whether the data is loaded.
[00:21:20]
And it would, when the response would come back, it would just override it and they would
[00:21:26]
see their new entity in there.
[00:21:30]
I love this recurring theme in this conversation that I think is one of those things that you
[00:21:36]
can always safely say, which is let the user experience guide your data modeling.
[00:21:45]
In one sense, it's the kind of thing when a consultant says it depends.
[00:21:50]
It's like, all right.
[00:21:52]
But it's also the most honest answer.
[00:21:55]
There's no one size fits all approach to your data loading patterns.
[00:21:59]
It really does depend on how, like, is stale data a problem or is it acceptable?
[00:22:07]
Is it okay for the user to click on something to reload?
[00:22:10]
Does the user need to know that data is being refreshed?
[00:22:14]
That's going to inform what your data modeling looks like as well.
[00:22:18]
I'm just stuck on the fact that you basically said that consultants are not honest.
[00:22:23]
No, they're like, when they're honest, it's boring.
[00:22:28]
Because they say, give me an answer.
[00:22:31]
And they say, well, I can't give you an answer because it depends.
[00:22:36]
Like the answer is, well, it depends on these things and these trade offs and which ones
[00:22:41]
do you want to make and these are some options.
[00:22:45]
Sometimes the honest thing is you can't just say, this is the way to do it.
[00:22:49]
Always.
[00:22:50]
I find this is true also in in house applications and in house developer teams.
[00:22:57]
We always throw around different ideas and like, if you want, we can make it do this
[00:23:03]
thing and this thing.
[00:23:05]
But you as the product manager, you have to really tell us what do you want.
[00:23:10]
And when we know how it should behave, then we can say, okay, let's say we need a new
[00:23:16]
remote data constructor that allows, let's say, canceling requests.
[00:23:22]
Right.
[00:23:23]
Or if you absolutely cannot put things on the screen that are stale without showing
[00:23:31]
that they're updating.
[00:23:33]
You could imagine an application where that's very important.
[00:23:37]
Someone's bank account.
[00:23:39]
Maybe you're like, don't show stale data.
[00:23:42]
We would rather have a loading spinner than stale data in that use case.
[00:23:46]
But if it's like social media posts and it's like, this isn't the latest version of the
[00:23:53]
newsfeed.
[00:23:54]
You want to get those things in front of the user as fast as possible and you don't want
[00:23:58]
them to be looking at loading spinners because then they're going to switch over to do some
[00:24:02]
other more productive thing with their life.
[00:24:05]
Yeah, exactly.
[00:24:07]
Yeah.
[00:24:08]
I'm sure that in the store itself, you can make it so that it automatically refreshes
[00:24:15]
some of the data.
[00:24:16]
I think you could do that because you have the full power of the update function.
[00:24:21]
So you can return commands and so you can do this kind of timer with commands or you
[00:24:28]
could even have the store have subscriptions.
[00:24:31]
Why not?
[00:24:32]
So definitely possible.
[00:24:34]
It's not something I have tried, but you definitely could have just, you know, after five minutes,
[00:24:41]
just refresh it just to be sure.
[00:24:43]
Right.
[00:24:44]
And then also like, I mean, yeah, that is kind of the cool thing about this pattern
[00:24:48]
is that it's a pattern.
[00:24:50]
It's not a library.
[00:24:51]
You can choose not to use it for some of the purposes.
[00:24:54]
Yeah.
[00:24:55]
And you could customize it to your needs entirely.
[00:24:57]
So you could have like your store, you could give it some state that anytime a page changes,
[00:25:06]
you could say I'm on a new page and you could use that state to see has the page changed
[00:25:12]
since the last time the store was refreshed to do any new requests.
[00:25:17]
And then you could have some store actions where you say, all right, if I've gone to
[00:25:22]
a new page, then this store action, if it's the first time performing this store action
[00:25:29]
since a page change, I want to reload data to make sure it's not stale.
[00:25:33]
So like really it's up to you.
[00:25:36]
And that's one of the cool, cool things.
[00:25:38]
It's just a more general pattern of, I think having sort of application context, right?
[00:25:44]
It's kind of a special case of this application context pattern, which more and more, I think
[00:25:50]
it's quite a nice pattern in Elm actually having some application context that is not
[00:25:55]
managed by every single page.
[00:25:57]
Yes.
[00:25:58]
It is basically just a thing that you pass from the level to the pages as they need it.
[00:26:04]
So there might be pages that don't have the store, there might be pages that only need,
[00:26:09]
let's say a specific record of the store.
[00:26:11]
It's just data, right?
[00:26:13]
It's just something that the top level model has, but not all the pages.
[00:26:20]
And to clarify what these data requests are doing, for every page in your application,
[00:26:26]
you're defining these data requests, which can give back a list of actions, which is
[00:26:33]
just a custom type.
[00:26:35]
It's similar to an effect type and it represents go get the list of users, go get the specific
[00:26:41]
data for this user ID, just a custom type that the store can receive and decide to go
[00:26:47]
fetch data and then insert it into this record.
[00:26:52]
And that data requests function receives the store.
[00:26:56]
So it can depend on pending requests and check if this is pending, I can't tell you what
[00:27:03]
to get yet because I need to know something from this user record which hasn't loaded
[00:27:09]
yet in order to tell you which list of friends to go get or whatever.
[00:27:17]
So you can depend on pending data because it calls data requests every time the store
[00:27:22]
updates.
[00:27:23]
Yeah, and you can even go a step further and you can just conditionally not return something
[00:27:31]
because you can't get at the data, right?
[00:27:33]
It's not successfully loaded yet.
[00:27:35]
So you just return a list with less items.
[00:27:41]
And whenever something happens to the store, the function will get called again.
[00:27:49]
So you don't really have to indicate in any kind of way that I haven't told you the whole
[00:27:55]
story yet.
[00:27:56]
You can just say, hey, for now, I need this.
[00:28:00]
And if you ask me later, maybe I will need more things.
[00:28:03]
But it's just a function from the store and the route and whatever into a list of requests.
[00:28:10]
Right, that's the declarative part.
[00:28:11]
It's kind of idempotent because if basically if you say I need to go get the list of users,
[00:28:18]
that's the store action that you are returning in your data requests.
[00:28:23]
If the web data variant is loading, then it says I'm already loading that.
[00:28:28]
If the variant it has for that for the users in that store record is not asked, then it
[00:28:35]
says, oh, I need to go do this.
[00:28:38]
If it's loaded, if it's success, then it says, oh, I already have this.
[00:28:43]
So it's idempotent.
[00:28:45]
You can declaratively just say what you depend on because it's not performing anything.
[00:28:51]
And that's actually where the defunctionalization part comes in, that it's these effects as
[00:28:55]
data because you're not doing it.
[00:28:57]
You're describing what you need declaratively.
[00:28:59]
Yeah, you are just giving it a recipe and it is going to the store for you.
[00:29:05]
To the shop.
[00:29:06]
Oh, the store.
[00:29:07]
That's a nice one.
[00:29:08]
I'm proud of myself.
[00:29:12]
Also to illustrate this a little bit more, you just get the store in your view function.
[00:29:19]
So if you say store.users, you have to do a web data to say case success or whatever,
[00:29:29]
show loading spinner.
[00:29:31]
If you don't return the action that is going to tell it it depends on users in that page
[00:29:37]
and it hasn't been loaded, then you're going to get an infinite spinner because you haven't
[00:29:41]
declared those dependencies.
[00:29:43]
Yeah, you might also, it depends on what you want to render for the different remote data
[00:29:56]
cases.
[00:29:57]
So for example, I usually show the spinner just for the loading case.
[00:30:02]
But then what do you show for the not asked case?
[00:30:05]
You might show nothing like text and empty string, but then it's kind of hard to figure
[00:30:12]
out that something is wrong.
[00:30:15]
The user doesn't really see that anything's happening, but also no data is showing up.
[00:30:20]
So yeah, it's kind of you need to find these cases when developing the page, right?
[00:30:30]
So you need to figure out, oh, I depend on users, but I haven't declared that I need
[00:30:37]
them.
[00:30:38]
So if somebody doesn't go to this page from the homepage, let's say, or via some flow,
[00:30:44]
if they go to my URL straight away, they are not going to get the data.
[00:30:49]
And so the store doesn't really have a way of checking that for you.
[00:30:56]
And while it isn't really a problem in practice, because usually you just figure it out and
[00:31:02]
you hit this issue during development, I think it would be nice to have integration tests
[00:31:11]
catch this.
[00:31:12]
So the integration test doesn't really care about any in it or data requests or anything,
[00:31:19]
but the integration test just says, go to this URL and expect that you will see some
[00:31:26]
loaded data on the page or that some requests have been made.
[00:31:30]
And with Elm spec or with Elm program test, I think that's very doable.
[00:31:35]
So it might be like a sister best practice next to this store by them.
[00:31:42]
That's a great point.
[00:31:43]
Yeah, because I imagine you do get into this situation where you, because you have global
[00:31:49]
application state that's shared between pages for all of your web request data, you could
[00:31:58]
get into a state that's different if you do an initial fresh page load versus navigate
[00:32:06]
from another page, because you might be depending on data that another page said it depended
[00:32:11]
on.
[00:32:12]
And I'm thinking, I'm wondering whether we could have some kind of Elm review rule to
[00:32:19]
catch those like store accessors and say, Oh, you are using store.users, but I don't
[00:32:25]
see get users in your data requests function.
[00:32:30]
So that might be something to explore.
[00:32:35]
My mind was going to like a, almost like a decoder style or something where your helper
[00:32:43]
for accessing things from the store.
[00:32:45]
Like if you say like store.end map to build up getting data from the store, and then that
[00:32:56]
store value would not only have the web data for whether it's a loading thing or a success
[00:33:02]
thing, but it would also have the recipe for knowing how to go and request itself.
[00:33:09]
And so you say, you know, you could do store.map five with the things you need and you reuse
[00:33:15]
that same thing to say, here's the data to load.
[00:33:19]
And here's how I turn my data into something that I receive on the page.
[00:33:23]
Are you thinking of data sources from Elm pages?
[00:33:27]
Just any sort of like a GraphQL selection sets, it's like the same idea, right?
[00:33:33]
In GraphQL, a selection set represents both here's the data to get, and here's how I turned
[00:33:38]
that into my Elm data type, the decoder part.
[00:33:41]
Yeah.
[00:33:42]
That's interesting to think about.
[00:33:43]
I believe it could work if your only accesses would be from the update function or let's
[00:33:49]
say from init or wherever where you can send those commands to then, you know, load the
[00:33:55]
data, but I'm not really sure how that works in Vue because you can try to reach for a
[00:34:05]
data in Vue, but you have no way of signaling, okay, I need you to load this, right?
[00:34:12]
Yeah.
[00:34:13]
You could try and decode it, but then it would not initiate the actual fetching of the data
[00:34:20]
if you only have it in the Vue.
[00:34:21]
You would either need to be sure that you reuse that definition of like your sort of
[00:34:28]
store decoder, which you could do by having a constructor for your like page that takes
[00:34:36]
the record of your Vue and init and your, you know, store decoder.
[00:34:41]
That would be one approach or you could have, or you could potentially have your Vue return
[00:34:46]
something besides just an HTML document or whatever.
[00:34:50]
Yeah.
[00:34:51]
Or your Vue return data that it depends on, but that gets a little bit weird.
[00:34:56]
Yeah.
[00:34:57]
And also probably a bit wasteful in like you would compute the view in the update, I'm
[00:35:01]
guessing or something like that.
[00:35:03]
Right.
[00:35:04]
Yeah.
[00:35:05]
Yeah.
[00:35:06]
I haven't thought that design through enough to know if it's a good idea or not, but I
[00:35:09]
do believe if somehow or other you can share this thing that tells you that the store decoder,
[00:35:17]
then it would work and I believe there are several good possible ways to do that.
[00:35:22]
I don't know if the Vue one is one of the good possible ways, but there are good possible
[00:35:25]
ways I think.
[00:35:26]
I agree.
[00:35:27]
Yeah.
[00:35:28]
Yeah.
[00:35:29]
That's exciting.
[00:35:30]
That calls for some kind of experiment.
[00:35:31]
Yeah.
[00:35:32]
I'm sure they will not sidetrack you from other projects.
[00:35:37]
Knowing you.
[00:35:38]
That's not possible.
[00:35:39]
I have no side projects.
[00:35:41]
Well, the cool thing is again that it is a pattern and so it can be explored.
[00:35:50]
People can explore this by like create it.
[00:35:52]
You've got an example repo that is a really cool way to sort of look at this pattern in
[00:35:58]
a more concrete way.
[00:35:59]
I definitely recommend taking a look at.
[00:36:01]
The talk is also a really good introduction to those ideas.
[00:36:05]
People can build their own patterns and write a blog post on a particular permutation of
[00:36:12]
the store pattern.
[00:36:14]
Yeah.
[00:36:15]
I'm pretty sure people are already doing some version of this.
[00:36:18]
Yeah.
[00:36:19]
Just that nobody is talking about it and I guess the COVID season didn't really help
[00:36:23]
with no L conferences being around.
[00:36:26]
Perhaps we would have heard about it from somebody else already.
[00:36:30]
I've worked with code bases that use this pattern as well actually.
[00:36:34]
That's definitely out there in the wild.
[00:36:36]
I mean you've got to imagine like people building production album applications, some use cases
[00:36:43]
it's going to be important to like persist some data between page changes instead of
[00:36:48]
throwing it away.
[00:36:49]
So it's natural.
[00:36:51]
So because this is a pattern that you can customize, I feel like it's pretty useful
[00:36:58]
to build it over and over again in each project.
[00:37:02]
Would you like to see this in a library?
[00:37:04]
Would you like to see someone publishes this package or does that feel not useful to you?
[00:37:11]
I haven't thought about it.
[00:37:13]
I think you would need to either your library would only solve your particular problem set.
[00:37:22]
So you would need let's say to show a spinner for reloading stale data but somebody else
[00:37:31]
would need it.
[00:37:32]
So I think either the library would be too narrow in its use cases or you would need
[00:37:38]
to somehow figure out what are the different use cases that people can have.
[00:37:46]
What are the different variations of this pattern and then perhaps have some kind of
[00:37:52]
custom type for customizing this.
[00:37:55]
I haven't tried doing this.
[00:37:57]
So it might be possible.
[00:37:59]
It might be nice to somehow let people easily use this but I think there's value in having
[00:38:07]
this in your code base and being able to just tinker around with it as your requirements
[00:38:15]
change.
[00:38:16]
So yeah I haven't felt the need to make a library out of it even though I have implemented
[00:38:21]
it in a few applications already.
[00:38:24]
I think it is just a pattern.
[00:38:27]
Hopefully there isn't enough in it to be a library.
[00:38:31]
I mean you can do a library for very tiny things.
[00:38:35]
Oh yeah.
[00:38:37]
Yeah but you can't extend it then.
[00:38:40]
I think like I remember Richard making a point in a talk once about like you know the example
[00:38:49]
he used was the HTTP builder package and he said the HTTP builder package is a very nice
[00:38:55]
little package but you know what's nicer is having some logic that knows the particular
[00:39:02]
endpoints to hit in your domain that it can have that knowledge abstracted in a high level
[00:39:08]
nice way and it knows particular types of headers.
[00:39:12]
So instead of like HTTP builder dot with header well you know what's nicer than with header
[00:39:19]
is you know with user session and with whatever specific types of headers your application
[00:39:26]
uses.
[00:39:27]
So his point was sort of like we don't always need to have a package published for a specific
[00:39:33]
thing custom tailoring them in Elm is so nice and relatively easy and so sometimes these
[00:39:39]
things are better as blog posts and example repos that illustrate the pattern than packages.
[00:39:46]
I think this is maybe one of those cases.
[00:39:48]
Yeah I agree.
[00:39:49]
I even I think I have tried this once not with HTTP builder but just with the HTTP library
[00:39:58]
where I was tired of always calling it with tracker equals nothing timeout equals nothing
[00:40:05]
and so on and so I just created tiny little abstraction where instead of that I need to
[00:40:10]
give it JWT token and it does the header automatically right and these little things it's not that
[00:40:18]
much of a code but it makes your life so much nicer and it's very tailored to the application.
[00:40:28]
It also removes a lot of common mistakes that you can do but just by forgetting to add the
[00:40:34]
authentication header in one of the calls.
[00:40:37]
So yeah a lot of nice developer experience improvements.
[00:40:41]
And sometimes like giving these patterns a name and giving people example repos gives
[00:40:48]
them something to latch on to so they say oh you know this is what I need to improve
[00:40:52]
this code.
[00:40:53]
It does seem like this store pattern it does make me think like it's the sort of cookie
[00:40:58]
cutter wiring that LMSPA tries to solve and I wonder if like it would make sense to support
[00:41:07]
something like that in LMSPA or to support a more general concept of like application
[00:41:13]
context.
[00:41:14]
Like the shared model so I guess maybe with LMSPA you could already do that but I'm not
[00:41:20]
sure if you could accomplish the task of automatically wiring in the sort of data requests part from
[00:41:26]
each page.
[00:41:27]
So that's an interesting thing to think about.
[00:41:30]
Yeah, yeah.
[00:41:31]
Sounds interesting.
[00:41:32]
I don't have many experiences with LMSPA to be honest but if you have the shared model
[00:41:38]
that seems like it would support it if you also have a way to then process the shared
[00:41:44]
model.
[00:41:45]
Right.
[00:41:46]
I have worked mostly with GraphQL in the past two years and with Elm GraphQL.
[00:41:51]
With GraphQL you usually only request what you need but this store pattern makes me think
[00:41:56]
that you are nudged towards loading everything that any page can use potentially asking for
[00:42:03]
way more than necessary when you make your HTTP requests.
[00:42:07]
So the store pattern, the store will only ask for the requests on the current page,
[00:42:15]
right?
[00:42:16]
So if you are on the homepage and it asks for one specific thing and there are different
[00:42:21]
pages that ask for different things and if you stay on the homepage it will never ask
[00:42:26]
for the other stuff.
[00:42:28]
So yeah I meant for instance page A wants to know this information from a user, page
[00:42:36]
B wants different information from a user.
[00:42:39]
So if you have the same users field in the store then both pages will load the same data
[00:42:47]
but only use parts of it.
[00:42:49]
Oh yeah I understand.
[00:42:51]
Yeah this is probably for discussion with your backend team because the store pattern
[00:42:59]
pretty closely follows the API requests, right?
[00:43:03]
So if the only way to get data A and data B for the same user is to get the full details
[00:43:11]
then the store has no other way to do that, right?
[00:43:15]
But if there are separate endpoints for getting let's say get user friends and get user images
[00:43:24]
then you could have two different fields in the store where all the data would be lazy
[00:43:31]
loaded and page A wouldn't need to ask for data that only page B cares about.
[00:43:38]
So yeah I think with GraphQL it's definitely better in that you can be very granular but
[00:43:46]
the store pattern in itself doesn't really dictate how your request should look.
[00:43:53]
It just follows what the API are giving you.
[00:43:57]
I feel like it doesn't say how the request should look but I think it dictates how the
[00:44:04]
data should look in your store.
[00:44:06]
So in that sense if a user is defined to be a record with a name and a list of images
[00:44:13]
and a list of friends then you need to fetch all of those.
[00:44:17]
And I feel like even if you use GraphQL I'm not sure how you would model that.
[00:44:23]
Or the fact that sometimes you want the list of friends and sometimes you don't want the
[00:44:28]
list of images.
[00:44:29]
I think you would just have different types with different responses.
[00:44:34]
So the user friends response could be user ID and friend IDs.
[00:44:43]
And the other one would be a different record and then the store would hold let's say dictionary
[00:44:50]
of user ID to web data of user friends.
[00:44:57]
And the other field would hold again dictionary from user IDs to web data of user images.
[00:45:05]
So I think it doesn't really dictate that.
[00:45:10]
You can definitely be as granular as you want but you will need to follow what the API is
[00:45:18]
able to give you.
[00:45:19]
It would be interesting to combine this pattern too with GraphQL.
[00:45:26]
You could imagine getting pretty clever with combining requests to say well I need these
[00:45:33]
three different things which I have selection sets for.
[00:45:37]
Let me merge that selection set and run the same decoder against them.
[00:45:44]
Let me intelligently combine these three selection sets together so I can perform them at once.
[00:45:52]
I think that's very doable if you can combine different selection sets together.
[00:46:01]
You can just convert each request from the store to a selection set and then just list
[00:46:07]
fold them together.
[00:46:09]
I think that's a reasonable optimization and I think the store pattern is compatible with
[00:46:16]
GraphQL although I haven't tried.
[00:46:19]
The one thing that seems like it would get a little bit awkward is the relations because
[00:46:25]
like I always say GraphQL has two parts, the query language part, QL and the graph part.
[00:46:30]
And the graph part is very different than what we're used to in most REST APIs where
[00:46:36]
we're sort of usually getting lists of IDs and then making a follow up request to get
[00:46:42]
those.
[00:46:43]
And the way you talk about the store pattern and the demo you gave in your talk, it's pretty
[00:46:51]
natural to have a dictionary of IDs to web data of user or some sort of thing you're
[00:46:58]
loading selectively where you go to a user's profile page and you load that one user.
[00:47:05]
While with GraphQL it tends to be more natural to describe those relationships as a single
[00:47:11]
request rather than independent things that you kick off and store independently.
[00:47:18]
That's the one thing I can imagine getting a little bit awkward where it seems to fit
[00:47:22]
pretty natural with that REST style pattern and then storing things selectively.
[00:47:28]
I think I see what you are saying.
[00:47:32]
So GraphQL does support what the store requests want, but GraphQL does much more, right?
[00:47:43]
You can describe what you will want, what you want in one go, but the store has no way
[00:47:51]
of doing that.
[00:47:52]
The store has no way of saying, I want the user and then whatever images he has, I want
[00:47:59]
them all.
[00:48:00]
So the store needs to go in two passes, but GraphQL could have done it in one pass.
[00:48:07]
Exactly.
[00:48:08]
And the store is in a sense optimized to be able to do that more granularly, whereas GraphQL
[00:48:16]
is more optimized to do it less granularly and say, this is exactly the data I need right
[00:48:21]
now, go and get it.
[00:48:22]
So they're a little bit tugging in different directions somewhat.
[00:48:29]
If the data requests describes what you want the store to load, then surely you can describe
[00:48:38]
it in a GraphQL way.
[00:48:41]
Just translate that to a GraphQL request saying, Hey, I want the users and their images.
[00:48:46]
Like you could probably do that, right?
[00:48:49]
You could have a special custom type constructor that says this and then interpret it in that
[00:49:02]
way.
[00:49:03]
Yeah, definitely.
[00:49:04]
That'd be a cool experiment.
[00:49:05]
I would love to see a little fork of Martin's repo for the store pattern to illustrate what
[00:49:13]
it would look like with GraphQL and if it would work well with things like relations,
[00:49:16]
that would be really interesting.
[00:49:17]
And exercise for the listener.
[00:49:20]
If the listener does it before we release this episode, then we can add it to the show.
[00:49:26]
Wait, there's always Twitter.
[00:49:28]
We can ask him on Twitter.
[00:49:33]
One thing that I'm curious about is how do you pass the store around in practice to all
[00:49:40]
the view functions?
[00:49:42]
Do you pass in the store or do you pass in extensible records saying I want these fields
[00:49:50]
from the store or as separate arguments, I want the images and I want the friends.
[00:49:55]
What do you do?
[00:49:56]
I believe all of these would work.
[00:49:58]
And thinking about the classic Richard's talk where he says about like extensible records
[00:50:06]
are basically only nice in this view use case.
[00:50:12]
I think that would be a good idea.
[00:50:15]
Although I tend to skip extensible records.
[00:50:21]
So the main gives the store to the pages and then in the pages, it's not as clear as to
[00:50:29]
whether the page still gives the whole store to some kind of view sub function or whether
[00:50:37]
it just gives them a specific dictionary, let's say.
[00:50:42]
It varies.
[00:50:43]
And I want to know what you did in practice.
[00:50:48]
So the innermost children usually receive just a specific dictionary or web data, but
[00:50:57]
it might have been nicer if store was just passed everywhere and the functions were to
[00:51:05]
limit what they can take from it with extensible records.
[00:51:08]
I think it's viable.
[00:51:11]
I feel like the decoder style would be the best of both worlds there where you say what
[00:51:16]
you depend on in a way that you eliminate this type of out of sync error where the data
[00:51:24]
dependencies cannot get out of sync with the data that you actually depend on in the view.
[00:51:29]
And you don't just have this unconstrained store where you have all these things, but
[00:51:35]
you don't know if you depend on them or not.
[00:51:37]
You have exactly what you depended on in the view.
[00:51:40]
If you need to depend on a new thing, you don't need to go fuss with extensible records
[00:51:47]
and have the issues where the extensible record gets out of sync with the main definition
[00:51:53]
of the record, which is pretty painful, right?
[00:51:56]
But you own the record of your type alias for the decoded thing of the data you depend
[00:52:04]
on for your page.
[00:52:07]
Maybe it's time for Elm revolution and where we start to make the views a little bit smarter.
[00:52:15]
Because I think right now we tend to make the view just return HTML and it's idempotent,
[00:52:25]
it's stateless, just convert the model to some data.
[00:52:32]
But perhaps there are nice things to be found if we let the view return a bit more or if
[00:52:38]
we build some kind of little abstraction on top of HTML that actually tracks what data
[00:52:44]
we use.
[00:52:45]
It's very interesting to think about.
[00:52:48]
I have no idea where that would lead.
[00:52:51]
You are probably the person with the most experience with this, Dillon, for Elm pages.
[00:52:57]
Yeah, right.
[00:53:00]
I like this tangent.
[00:53:02]
This is a very interesting tangent.
[00:53:04]
There's an interesting, highly experimental repo.
[00:53:13]
It's not in a stable state.
[00:53:14]
It's a proof of concept.
[00:53:16]
But there's a project called Elm Suspense.
[00:53:21]
It is an interesting exploration for essentially like...
[00:53:24]
Don't tell us what it is.
[00:53:25]
Leave the suspense.
[00:53:26]
Suspense achieved.
[00:53:27]
Now you can go.
[00:53:32]
Well, React had the ultimate suspense with leaving people in suspense for years.
[00:53:40]
Until they finally released Suspense.
[00:53:45]
Their users didn't know how to react.
[00:53:51]
So what is Elm Suspense about?
[00:53:52]
I'm not really curious.
[00:53:55]
It has a more powerful view concept, kind of like what you're talking about, this revolution
[00:54:01]
where the view can sort of describe what it depends on and you can kind of get these loading
[00:54:11]
states until the data is retrieved.
[00:54:15]
So that's a really interesting example.
[00:54:18]
Of course, I've been thinking about this in a somewhat different way with Elm pages where
[00:54:26]
you have this abstraction of a data source, which allows you to sort of declaratively
[00:54:31]
say, I depend on this data.
[00:54:34]
And it's not this imperative thing where you wire up messages and say, when I get this
[00:54:38]
thing.
[00:54:39]
And the most exciting thing to me about it is that you don't have intermediary states
[00:54:44]
that you have to manage.
[00:54:46]
So you don't have to do loading spinners.
[00:54:49]
And you don't have to manage those loading spinner states in your view.
[00:54:53]
The reason is because you move the loading state to the server because the server goes
[00:54:59]
and you say, I have this cookie that gives me the user's session ID.
[00:55:06]
I'm going to go look up that session ID from the database.
[00:55:09]
I'm going to go get the user's data.
[00:55:11]
I'm going to go get the specific data I need to load this page.
[00:55:14]
Now I've got all the data.
[00:55:16]
Now I return an HTML response with the data to hydrate the Elm application.
[00:55:21]
So there's no loading state in your Elm application because the loading state is just the server
[00:55:28]
building up the response to return.
[00:55:30]
So that's another interesting kind of revolution in how we can approach managing data in an
[00:55:38]
Elm application.
[00:55:40]
Maybe in some cases, it doesn't allow you to do this sort of fine grained control where
[00:55:44]
you can avoid doing data fetching between page loads.
[00:55:49]
So you go to a new page, you get new data.
[00:55:52]
The Elm pages 3.0 answer to that question would be more, well, have really good caching
[00:55:58]
on your server so that that's not a problem.
[00:56:01]
In some use cases, maybe that's not a good answer.
[00:56:04]
But I think that's another interesting avenue to explore that I think is pretty exciting.
[00:56:08]
Yeah, that's nice.
[00:56:10]
So should we try to develop a Elm pages store or an Elm library?
[00:56:17]
Well, I mean, it kind of is a built in store in a way, you know, it's I mean, it has this
[00:56:23]
data layer built into it.
[00:56:26]
And it's a more declarative data layer.
[00:56:29]
But yeah, the data store is an interesting idea.
[00:56:31]
The this sort of suspense concept of having view like smarter view functions is very interesting.
[00:56:38]
Another thing I've been exploring with Elm pages is the sort of optimistic UI for states
[00:56:45]
where you're fetching things.
[00:56:47]
So if you're if you're creating a to do list item, Elm pages 3.0 has has a way to sort
[00:56:53]
of see that pending form post as you're doing it so you can so that can show up in an optimistic
[00:57:00]
UI.
[00:57:01]
But that's another area I was wondering about Martin is, do you have any thoughts on pending
[00:57:07]
mutations, things that change the server and how that could like purge parts of the store
[00:57:13]
and how that pending UI could be displayed in the UI and be included in the store?
[00:57:19]
Yeah, definitely.
[00:57:21]
I believe you can mostly do optimistic updates with the store pattern.
[00:57:28]
So in the store, you would insert into the dictionary at the same time as you send the
[00:57:36]
command to create the data right to create the entity.
[00:57:41]
And in that way, in that way, you would see it automatically created.
[00:57:47]
But there can be some it can sometimes be tricky if you need an ID for the new entity.
[00:57:56]
And so what would you what would you put there as a placeholder?
[00:58:01]
So I think this could be doable.
[00:58:04]
It's definitely a solvable problem.
[00:58:06]
You could have another field in the store where there are like users being created or
[00:58:11]
something and the view could check both.
[00:58:15]
You could check the main field primarily.
[00:58:20]
And then if it doesn't find the user there, it could check the users in flight, let's
[00:58:26]
say.
[00:58:27]
And so you could show user even if they don't have an ID yet.
[00:58:33]
So I believe it's doable.
[00:58:36]
And again, it depends on what your application needs.
[00:58:39]
We are coming back to that to that sentence.
[00:58:43]
So yeah, you would just basically again, because it's your application code and you have full
[00:58:52]
control over the store, you can reach into the interpretation of that message.
[00:59:00]
So you can you have full control over how it will send the command, whether it will
[00:59:08]
check for data that's already there.
[00:59:12]
You can say it would let's say if it wasn't a get command, but it was a delete command,
[00:59:21]
you could just delete the dictionary key right there.
[00:59:25]
And then in the failure case, you could bring it back, right?
[00:59:30]
Because you still have all the data.
[00:59:32]
So it depends on whether that's worth it and whether your application needs that kind of
[00:59:39]
snappiness.
[00:59:40]
But I can see it being supported by the store command.
[00:59:44]
Again, it's not that much.
[00:59:46]
The store command is not that much about telling you how to do stuff.
[00:59:50]
The store command is really just where the data lives.
[00:59:53]
So it can do all the things that you could have done without it.
[00:59:59]
I'm now imagining a store where one of the data requests is deleting something.
[01:00:04]
So does it sound like a very good idea?
[01:00:08]
So get commands would probably not want to delete something.
[01:00:12]
But again, the custom type in your store with the data requests, it doesn't have to be only
[01:00:19]
for getting stuff.
[01:00:23]
It is more about what the API can do.
[01:00:27]
And so you can have get users and get a specific user, but you can also have create a user,
[01:00:34]
you can have delete user, update user, and these all live in the same kind of type.
[01:00:40]
And you just don't really use those in data request function.
[01:00:45]
But you definitely can use them from within update of these pages, right?
[01:00:51]
Oh, that's a cool distinction.
[01:00:54]
I hadn't put my finger on that.
[01:00:56]
But that makes sense.
[01:00:58]
I think what Jeroen is sort of hinting at with the anxiety around using store for this
[01:01:04]
is like, if you're like, delete user, and then is it going to keep trying to delete
[01:01:10]
or is it going to keep trying to create something and somehow the state gets cleared out to
[01:01:16]
a not added state and it keeps creating?
[01:01:21]
I think for extra safety, you could split this type into like get requests and mutations,
[01:01:30]
let's say, right?
[01:01:31]
Right, right.
[01:01:32]
Queries and mutations.
[01:01:33]
We are ever so closer to GraphQL now.
[01:01:38]
Yeah, very interesting.
[01:01:40]
But at the same time, like, what I'm hearing is that there's value to still using the store,
[01:01:47]
not necessarily the data request part of the store where you say, hey, if I'm on this page,
[01:01:51]
this is the data I'm going to need.
[01:01:53]
But having it be able to interact with the store model where it can kick off request
[01:01:58]
through update, like you're saying, and then that allows you to implement that logic that
[01:02:05]
has access to the whole store.
[01:02:07]
And it can purge parts of it.
[01:02:09]
If you're deleting something, it can clear it out, it can, you know, do whatever logic
[01:02:15]
it needs to do, and it can reuse that same central point where you're defining all of
[01:02:20]
your logic for your HTTP requests.
[01:02:22]
Yeah, yeah, it's definitely not just for read only access.
[01:02:26]
It's for the whole way of how you are working with HTTP.
[01:02:32]
So do you have thoughts on, we talked a little bit about defunctionalization, but maybe we
[01:02:37]
can clarify a little more.
[01:02:39]
And so you define this custom type action, and that's going to have a variant for everything
[01:02:46]
you can do, and the variant is going to have a parameter for any data it needs.
[01:02:52]
So if it's like get user, it's going to have an ID in there because it needs that to go
[01:03:00]
do the lookup, to do the get request.
[01:03:02]
So you want to just define defunctionalization, first of all?
[01:03:05]
Okay, I can try.
[01:03:08]
So in my mind, defunctionalization is a type of refactoring where you have a place in your
[01:03:18]
code that uses a function and somebody else passes that function in, right?
[01:03:24]
And let's say that there are three such different functions, and what you do is create a custom
[01:03:30]
type that lists those use cases, and then you replace the function with the custom type.
[01:03:37]
So now instead of passing functions around, you are passing the custom type around, and
[01:03:42]
at the last possible step, you then convert the custom type back into that function and
[01:03:49]
run it, right?
[01:03:50]
So it seems like a lot of work for no benefit.
[01:03:54]
It seems like busy work, but it does have benefits because you now have a custom type
[01:03:59]
that you can kind of look into, right?
[01:04:02]
It's not an opaque function.
[01:04:04]
Every function, you don't really have any idea what it does until you run it, and with
[01:04:09]
the custom type, you can filter those out.
[01:04:12]
You can...
[01:04:13]
And we do it in the store pattern, right?
[01:04:16]
We check whether we need to run it, and if not, we just throw it away.
[01:04:20]
We couldn't do that with the function.
[01:04:21]
If we just passed the command functions that run the HTTP requests, we couldn't really
[01:04:30]
work on those in that kind of way.
[01:04:33]
And yeah, so I think it's really a nice trick to have up your sleeve.
[01:04:39]
And in the Elm community, we are already doing the functionalization every day.
[01:04:44]
So instead of message being a function from model to model, we just have a message custom
[01:04:51]
type that we can then interpret in...
[01:04:55]
We translate it to a function, but you could do so many more things with it.
[01:05:00]
And so in the store pattern with these actions, we run them.
[01:05:05]
So we convert them to the command returning functions, but also you could have really
[01:05:13]
nice user experience for the failures, right?
[01:05:18]
So I have this in the store example repository where if we get HTTP failure, we also remember
[01:05:28]
the action that caused it.
[01:05:31]
And then you can convert the action to, let's say, the endpoint, right?
[01:05:38]
To the HTTP method.
[01:05:39]
You can derive the request JSON body again and show it to the user.
[01:05:46]
And so there are many different ways how you can use the custom type, whereas with the
[01:05:52]
function, you could just run it and then it's all lost, right?
[01:05:56]
All the information, all the details are lost and you can't really rewind time back and
[01:06:01]
so on.
[01:06:02]
Yeah.
[01:06:03]
It's also much easier to test and you can also debug them, which will give you a much
[01:06:09]
more helpful message.
[01:06:12]
Yeah.
[01:06:14]
You could do analytics with them.
[01:06:16]
The sky's the limit.
[01:06:17]
So the data is nice.
[01:06:20]
Yep.
[01:06:21]
Yeah.
[01:06:22]
Maybe this is heresy, but you could imagine having the view message type like HTML message.
[01:06:30]
You could imagine it being HTML function from model to model, right?
[01:06:38]
And I'm sure some people do this, right?
[01:06:41]
But I mean, there's a talk at Elm Europe where they do this.
[01:06:44]
Oh, really?
[01:06:45]
Yeah.
[01:06:46]
I'm not familiar with that one.
[01:06:47]
Interesting.
[01:06:48]
Yeah.
[01:06:49]
I don't remember the name, but maybe we'll add it to Shenotes.
[01:06:51]
Yeah.
[01:06:52]
Yeah.
[01:06:53]
Definitely.
[01:06:54]
I have seen people on the Elms have this light bulb moment when they show you the model,
[01:07:02]
when they show you the message with the function from model to model and they say, Hey, this
[01:07:06]
is cool.
[01:07:07]
Now I don't need an update function.
[01:07:11]
And they are happy and this is basically like going, this is the functionalization, right?
[01:07:19]
Yeah.
[01:07:20]
Exactly.
[01:07:21]
Yeah.
[01:07:22]
Yeah.
[01:07:23]
So basically we like data more than we like functions in a functional programming language.
[01:07:31]
Here's my take on it.
[01:07:33]
I'm pretty sure I coined this phrase, but either way I like it.
[01:07:37]
It's wrap early, unwrap late.
[01:07:39]
As far as I'm aware, it may be someone else independently coined it, but I believe I coined
[01:07:45]
that phrase, but I love this idea, wrap early, unwrap late.
[01:07:49]
Basically the idea is you want to have the ideal representation of data and also one
[01:07:57]
that isn't lossy.
[01:07:59]
It doesn't lose information, right?
[01:08:02]
So like, I mean, it's like if you have, um, if you have a podcast, maybe how do you store
[01:08:08]
that recording?
[01:08:09]
You're probably going to store like raw wave files or some uncompressed thing.
[01:08:13]
That doesn't mean that you want to send like a one gigabyte file to listeners, but you
[01:08:19]
want that in the recording and the archive and then you derive the lossy format.
[01:08:25]
Um, but I mean, that's, that's true for many things, right?
[01:08:28]
Like maybe you need a JPEG somewhere or whatever PNG, but you might start with an SVG format
[01:08:35]
or something that's not lossy because who knows, maybe you need a higher resolution
[01:08:40]
version of that or whatever it is, right?
[01:08:42]
It's really a similar principle that with your data types, maybe you only need it for
[01:08:46]
one thing right now, but if you need it for something else later, it opens up your possibilities
[01:08:51]
when you have the lossless representation in the ideal format.
[01:08:55]
Exactly.
[01:08:56]
You, you don't want to limit your options too early.
[01:09:00]
And it's also very nice for like debugging and understanding the code and it's more like
[01:09:06]
inspectable by the developer.
[01:09:08]
You can look at it and say, what is the incoming message instead of just like, there are functions
[01:09:13]
being passed around.
[01:09:14]
What do you do about that?
[01:09:15]
You're like pull out your stepwise debugger, which we can't do in Elm.
[01:09:19]
Well it's very nice to do a debugger if you're passing around actions, which are just a custom
[01:09:25]
type with nice data.
[01:09:26]
Yeah.
[01:09:27]
I really do agree with the official Elm guy that says the custom types are basically the
[01:09:33]
most important part of Elm.
[01:09:36]
And purely.
[01:09:39]
And opaque types.
[01:09:43]
Which I've constrained myself not to mention that the store is a record and not an opaque
[01:09:52]
type.
[01:09:53]
I will not mention it and no one will know that this bothers me.
[01:09:59]
Well I do, I do think again, you know, we've talked about this before, Jeroen, the distinction
[01:10:04]
between opaque types sort of having an internal representation versus exposing a public facing
[01:10:13]
custom type.
[01:10:14]
And that is kind of an interesting, but I guess you could, so that is really interesting
[01:10:20]
because you could argue like, okay, let's have an opaque type for the store.
[01:10:25]
Let's have the actions instead of being just a custom type with all the variants exposed.
[01:10:30]
Let's have functions to construct each action.
[01:10:34]
But then you want to be able to inspect them in the outside world.
[01:10:37]
Because like in Martin's example, you want to show a toast message for an error and you
[01:10:41]
want to be able to describe what happened.
[01:10:43]
And you don't necessarily want that logic of presenting the toast error to live in the
[01:10:49]
store module.
[01:10:50]
That wouldn't make sense.
[01:10:52]
Exactly.
[01:10:53]
So I guess, but maybe this one breaks our intuition a little bit of, you know, how,
[01:10:58]
I mean, we should really change the name of this podcast to like opaque type radio, but
[01:11:05]
maybe that breaks our intuition a little bit.
[01:11:08]
Maybe that's a really good example where it truly does belong as a bi directional non
[01:11:15]
opaque custom type that you want to both create it and consume it.
[01:11:21]
And that makes sense.
[01:11:22]
I don't know, I'm still, I would still like to explore it within a big type.
[01:11:29]
I think it would be nice.
[01:11:31]
I think, I think it could.
[01:11:33]
And so I haven't tried it.
[01:11:35]
So I don't know what benefits would it bring.
[01:11:37]
I think in the application context, this kind of safety is generally less of a problem.
[01:11:45]
Or let's say you don't need the benefit it gives with like changing the implementation
[01:11:51]
underneath and so on.
[01:11:54]
But so, so in the application context, I think it's nicer to be able with the raw thing and
[01:12:01]
but, but you definitely could make the store opaque, make the message type not expose its
[01:12:08]
constructors and so on.
[01:12:10]
But you would then need to have these functions that let you decide, okay, do I need to show
[01:12:17]
a notification toast for this message or not?
[01:12:20]
Right.
[01:12:21]
So it's kind of, does it really belong there or not?
[01:12:24]
But you could do it.
[01:12:26]
And since it's all in your application, I think it would be fine.
[01:12:31]
Even this style with a function for toasts that lives in the store.
[01:12:38]
But I think it probably gives us an answer as to whether this would be nice as library
[01:12:43]
or not.
[01:12:44]
Because, because I think the library can't really decide that for you.
[01:12:49]
Right.
[01:12:50]
Ultimately you depend on a very custom custom type, the more of a bad fit it is for a library.
[01:12:56]
Yeah.
[01:12:57]
But this one definitely breaks my intuition a little on opaque types because it, yeah,
[01:13:02]
but I don't know.
[01:13:03]
Jeroen, what do you think about that?
[01:13:04]
Is it?
[01:13:05]
Opaque types is always the answer.
[01:13:07]
I want to see your exploration.
[01:13:10]
Report back to us.
[01:13:11]
All right.
[01:13:12]
When I have time.
[01:13:15]
Just one more thing that I didn't mention yet about the store pattern and that's the
[01:13:22]
derived data.
[01:13:24]
So thanks Dillon for refreshing that part of my brain.
[01:13:31]
It's really nice when the store is the single place where all the data lives.
[01:13:37]
Single source of truth.
[01:13:38]
Yeah.
[01:13:39]
It's really nice to, if you ever need it to have the derived data live in there because
[01:13:46]
you know that only the store will mutate the data.
[01:13:50]
Right.
[01:13:51]
And so I think it's generally better to not drive data and to let's say rather do it in
[01:13:59]
the view if it's not computationally intensive.
[01:14:04]
But if you need.
[01:14:05]
I think you said to not drive data, but I think what you mean is to not store derived
[01:14:09]
data, right?
[01:14:10]
It's better not to store derived data.
[01:14:12]
Yeah.
[01:14:13]
Yeah.
[01:14:14]
Yeah.
[01:14:15]
It's better not to store derived data because there is the danger of having it go stale,
[01:14:21]
right?
[01:14:22]
If you only ever update the main source of truth and you don't update the data, then
[01:14:29]
it gets out of sync.
[01:14:30]
So I think it's better to do it like temporarily, ephemerally in the view.
[01:14:36]
But if you ever need that and we like in my experience, we have had the need to store
[01:14:43]
different representations of the data.
[01:14:46]
It's really nice that you can just go look into specific cases of the update functions
[01:14:53]
for the store.
[01:14:54]
And do you know that, or at least there's very high chance that those are the only place
[01:14:59]
you need to check, right?
[01:15:01]
So if you get a list of users, you will save it into the users field and into the other
[01:15:10]
field.
[01:15:11]
I don't know what that would be, right?
[01:15:13]
But you store it into these two fields.
[01:15:17]
And then if you insert the user, again, only in the store will you need to insert the user
[01:15:27]
into the dictionary and into the other data structure.
[01:15:31]
So it's really nice that you don't really need to hunt these places around the code
[01:15:37]
base, right?
[01:15:38]
Yeah.
[01:15:39]
Just like in a pig type.
[01:15:41]
Yeah, kind of, almost.
[01:15:45]
This deriving thing, I think is one of the most important best practices in Elm code.
[01:15:52]
And I wonder if this would be like a nice Elm review rule where you look at things you're
[01:15:57]
storing in the model and say, am I depending on the model for things I'm putting back in
[01:16:03]
the model?
[01:16:04]
I mean, I guess you do need to update.
[01:16:07]
Could be interesting to look at.
[01:16:10]
But yeah, where would you start?
[01:16:13]
Because you do need to depend on the model for certain things.
[01:16:16]
So I don't know if there's a way to distinguish between derived state or really this is now
[01:16:21]
the source of truth, which depended on the previous model state.
[01:16:24]
Maybe there's not.
[01:16:25]
I don't know.
[01:16:26]
Maybe just like toggling something, for instance.
[01:16:30]
Yeah.
[01:16:31]
Perhaps we could have some kind of annotation for derived data, derived fields in the model.
[01:16:36]
And then the rule would check that everywhere you are changing this one, you also need to
[01:16:43]
change that one.
[01:16:44]
I don't know.
[01:16:46]
I mean, that's usually what I use a pig types for.
[01:16:49]
So I think maybe, maybe opaque types are the answer.
[01:16:52]
They are the answer.
[01:16:53]
And when it's not the answer, there's Elm Review.
[01:16:57]
There's an Elm Radio t shirt for you.
[01:17:00]
Opaque types are the answer.
[01:17:02]
But when they're not, there's always Elm Review.
[01:17:09]
My birthday is...
[01:17:15]
Well this was a pleasure, Martin.
[01:17:16]
Thanks so much for coming on.
[01:17:18]
If people want to follow you or find out more, where should they go to follow your work?
[01:17:24]
Yeah, it was a pleasure for me also.
[01:17:26]
So I am on all the usual places like Twitter, GitHub, the Elmslack under the nickname.
[01:17:33]
Well it's my surname, but under the nickname Janicek.
[01:17:38]
And yeah, I also have some Elm videos on YouTube and so on.
[01:17:43]
And I stream on Twitch, but it's not that often.
[01:17:45]
So I think the GitHub and Twitter are the best places to go.
[01:17:50]
They're great streams though.
[01:17:51]
I love your TDD streams.
[01:17:53]
You've done some nice ad winner code in the past.
[01:17:55]
Yeah, definitely check them out.
[01:17:57]
Also if you want to stumble upon Martin, you can just ask beginner questions on Slack.
[01:18:03]
Yeah, we are having races with Jeroen and other people on who answers the first.
[01:18:13]
Amazing.
[01:18:14]
People should definitely check out the recording of your talk on this.
[01:18:18]
It's a really great illustration and the example repo.
[01:18:21]
And Jeroen, until next time.
[01:18:24]
Until next time.