elm radio
Tune in to the tools and techniques in the Elm ecosystem.
Elm Store Pattern
Martin Janiczek joins us to discuss a pattern for declaratively managing loading state for API data across page changes.
Published
June 6, 2022
Episode
#58
Martin Janiczek (
github
) (
twitter
) (
youtube
)
Martin's
Store Pattern talk
Store Pattern example GitHub repo
Gizra fetch pattern blog post
elm-fetch, and Easier HTTP Requests to Reason with
Gizra
elm-fetch
package
RemoteData
package
RemoteData blog post
How Elm Slays a UI Antipattern
elm-program-test
is useful for integration testing data loading
elm-suspense
proof-of-concept repo
Okay to use Store pattern for mutations, just kick them off outside of
dataRequests
Defunctionalization
Wrap early, unwrap late
Derive from source of truth instead of storing derived data
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.