Resources

Shinystate (Eric Nantz, Eli Lilly) | posit::conf(2025)

shinystate: Launching collaboration and session management to new heights in Shiny applications Speaker(s): Eric Nantz Abstract: Shiny is being used more regularly as a front end to sophisticated workflows in data science, often with a large number of inputs and intricate reactivity. End users desire a way to save their progress through the application workflow and share their state of the application with others for review. For small applications, the existing Shiny bookmarkable state feature is a valid approach. However, many of the production Shiny applications I've created require more granular control. Enter {shinystate}, my new R package that offers an intuitive class system and integration with {pins} to let developers choose where to save application state, ability to save multiple sessions at once, and bring collaboration to new heights for end users. Materials - https://rpodcast.github.io/shinystate-positconf2025/ posit::conf(2025) Subscribe to posit::conf updates: https://posit.co/about/subscription-management/

image: thumbnail.jpg

Transcript#

This transcript was generated automatically and may contain errors.

All right. Well, hello, everyone. It is certainly very exciting to be back at another posit conference and talking about my favorite topic in the R community, the Shiny ecosystem. I'm really excited to be talking to you about my first R package that's seen outside of my company's firewall in over 13 years called Shiny State. So this has been a long time in the making.

I have been very fortunate that in my early days with Shiny, since I helped be an advocate for it in my industry, I have been able to use Shiny in not just prototyping stuff, but in some very high stakes, very important collaborations, often with many different functions for, say, a clinical team. And we're working together, literally sitting in the room, pulling up an application, and literally going through different input options, different parameters for our statistical modeling. And it comes at you fast and furious.

Everybody's got their idea, and I'm frantically trying to keep up with it all. And especially if we have another meeting, I have to bootstrap all this again, figure out what I did the last time we met. And it just was a lot to manage. And they want to get to these answers really quickly. And I didn't want to always be toggling around the different settings to get to where I was before.

So, and in my case, I'm building very complex applications. These are not just like with five or however many inputs. They look similar to one of these great examples from the Shiny gallery, which I highly recommend a check out. This is an app called Radar. It's a really nice one in the medical space. But I live this life a lot with the apps I develop.

And so, like I said, many inputs, it takes a lot of effort to get that off the ground pretty quickly in these different meetings. So you may be wondering, well, is there a solution out there? Well, in 2016, there quietly was a solution out there, and it was called Bookmarkable State. And Winston Chang right here is one that wrote the article that I've linked to in the QR code there. And I dare say, even as a Shiny user from back then, this came out kind of quietly, but boy, did it open a lot of ideas for me personally.

Bookmarkable state and its limitations

And I started thinking about what are the different ways we can do this? Because on the tin, you capture the state of your application, and you're able to restore it at a later point, exactly kind of the friction I was trying to solve. However, as time went on, as my applications got more complex, there was a little bit of things that it didn't quite do enough for me. And I wanna walk through some of that as part of the motivation here.

Bookmarkable State has two ways of recording the state of your application. One of them is called the URL method, and that is where basically you get a browser-type address to your app, but after the .com or whatever, you may see this on the sites you visit, there's a whole bunch of stuff after a question mark, and it might have some name equals something, some name equals something, and that can be great, again, for more of the small, medium-size-ish applications. It gives me vibes of back when I was doing my retro gaming and having to put password prompts in the game, The Resumer, I left off. But there is a limitation with that, where sometimes if you have so many inputs, the browser address bar literally can't handle that wide of those different type of inputs.

But that's not the only method. There is also the server-side method. This is where instead of the encoded in the URL, those different parameters, they're saved to disk, as literally files on the disk, but they're saved as files in the hosting server that you deployed your app on. Typically this might be Posit Connect, could be something else, but they're kind of locked away, so to speak. They are in these, like, only these directories in the system that only an admin has actual access to. That can be a problem if you want to do something with this state later on, especially from the user perspective.

So I was trying to figure out what are ways of getting around this, and then fortunately, definitely, I was one of the people that helped launch the Our Pharma Conference, as some of you in the room may have attended back in 2018 at Harvard. We had the great fortune of having the author of Shining Himself, Joe Chang, give a wonderful keynote about using, he called it interactivity responsibly in the pharma space. It's a really excellent keynote that we had, and in his example app that he shared with us, there was just this little hint of customization and bookmarkable state that I had never seen before. It was not documented anywhere, and I'm thinking, oh boy, okay, this may be on the way to solving my issues.

So I took that as inspiration, started writing custom functions at my deployed, the apps I'm working on, but over time, I kept repeating this pattern bit by bit, and I thought, boy, I have Hadley Wickham's voice in my head. I write functions multiple times, I keep copy pasting them over. Eric, you gotta write a package out of this. So that's where Shiny State comes in.

Introducing shinystate

This is my newest R package. I call it the way to supercharge Shiny's bookmarkable state capability with key enhancements. I'm gonna walk through a lot of this today, one of which is having more granular control over where we are actually putting the storage of these bookmarks. Also, we're gonna see a capability to add some optional metadata associated with this that could be really handy in certain situations, and then I am now starting to get on the object-oriented train a little bit after looking at Shiny for all these years and seeing how it leverages systems like R6, I started using R6 for this package as a way to expose the methods.

So let's look at how you get started with Shiny State. There is some important setup to do, and we'll walk through this a little bit in this simple example here. First, of course, you load the package like anything you would do in R, and then as I mentioned, Shiny State is based in the object-oriented class system called R6, so we do have a class called storage class, and you just need to initialize that before your main app starts to take off in the coding.

This part, actually, we start to get to this hybrid of what Shiny State does, but also what the traditional bookmarkable state does, where you do have to wrap your UI in an actual function with the request parameter. That's mandatory for Shiny bookmarkable state, and then like other packages in the Shiny ecosystem, I do inject some JavaScript kind of dependencies in it. I just have a little friendly use Shiny State function to put that on the UI side of things.

Then you get to the server side of it, and then there are basically under the hood a lot of these callback functions that I'm kind of overriding, so to speak, but you as an end user don't have to care about that other than knowing that you kind of have to register this in the server side function. I may change the name of this function later on, I'm not quite sure yet, but then after that, you just have your main server logic, and then this is important, too. When you wrap your app UI and server functions with a Shiny app function, you need to enable the bookmarking, but again, you're using the server version of bookmarking. This is not the URL method. We're not covering that with Shiny State, so it is important that you get this right, and I do have extensive documentation that walks through all this in detail.

Saving and restoring state

So with that, let's talk about the meat of the package, which is actually how do we actually save the bookmarkable state using this new package?

In essence, there is a method in the R6 class that you create, and it's called snapshot. Snapshot can just be run with no parameters, but as I mentioned earlier, one of the key things I'm trying to solve here is to attach some metadata associated with this, and as long as you can make a list out of it, and each element of the list is like a single element, it'll work here. So in this case, you might think of, okay, I might call a name of that session and the timestamp it was created, and that will, under the hood, give you kind of this nice, tidy data frame of the different sessions that you can save throughout the lifecycle of your session, so to speak, and you see I have three here in this example, but again, this may start to get your creative juices going a little bit of what we can actually do with this, with this information.

So that's how to save it. Now it's, and yeah, one thing to note here, that URL, you may be wondering, well, Eric, you just told me it's server-side method. What the heck is this URL? Well, under the hood, bookmarkable state does do a URL method, but when you save the server, it just uses a hash, kind of like a git commit hash, to identify what that save-the-disk kind of collection is. So you can think of that as your ID of sorts for your session, but then that optional metadata, you might call that the human-readable side of what's distinguishing one row from another.

So how do we restore that? So the same R6 class has a method called getSessions, which will give you that data frame I just showed you on the previous slide, and with that, as long as you can find the particular URL in that session data frame that you wanna get, and you could use some simple tidyverse code like we do here, if you know you wanna get that session called exploring, and then pull that out as an object, that's the only parameter required in the session restore method. Just give it the URL, and then Chinese state will take care of the rest.

Sharing sessions with pins

I think the pins package has been an underrated superhero in the R ecosystem that is, you know, it's claimed the fame was, of course, giving a way to share a data frame quickly across different stores or locations, but ever since the tidy models team has started to use pins with, say, the vetiver ecosystem to be able to share like a compiled machine learning model, I thought, well, wait a minute, pins isn't just for data frames anymore, is it? You can actually do custom things in a pin because it lets you do a more flexible custom format that could be a bundle of objects, if you will, and so with respect to ShinyState, if you have a pin board, which is kind of like the way to define where you save your pin, you bring the board, ShinyState will take it.

So in this example, there is a board for what's called S3 object storage. That could be in AWS or Azure or whatever have you. If pins supports that board, ShinyState can work with it. This is gonna be very huge for what I talk about later on because now you can, again, stand on the shoulders of pins to determine where you wanna put this type of metadata associated with these sessions. I mean, there's lots of different possibilities. If you haven't heard of pins before, this is my big plug for that. It is really underrated in this ecosystem here.

If pins supports that board, ShinyState can work with it.

Collaborative session sharing in practice

So I've shown you some code snippets. I've shown you kind of some ideas. I'm hopefully planting the seeds. Let's see how this might actually work in practice. This is where I think we really start to power up here is the idea of sharing sessions. And let's have a fun example here.

There are some two scientists that I know kind of well. One's name's Dr. Thomas Light. One's name's Dr. Albert Wiley. They have a good relationship early on, but then that Dr. Wiley guy tries to take over the world 11 times. Let's not remember that right now. But let's say they're collaborating on building the next AI-powered robot, for example. And they want a simple Shiny app where, say, Dr. Light can start tweaking some parameters and then he wants to share it with Dr. Wiley to kind of take over later on. Maybe they complement each other quite a bit.

So let's say they have this retro-inspired Shiny app here where they have some simple parameters. They give the name of the robot, different attributes of it, their power, their speed, armor, whatever have you, whatever weapon they have. And then Dr. Light says, okay, I like what I've done here. I wanna name this session and be super boring and call it draft one or whatever. But notice this checkbox here, share session. And he's gonna say, okay, I wanna save it out. And there you go, it's successfully saved. Nothing radical here. Again, I haven't really done too much different than what the default Shiny bookmarkable state does, but here comes the fun.

Let's say Dr. Wiley logs into the app the next day and he wants to see what Dr. Light shared with him. So he hits this little restore session button and he sees that session listed in the app. Hits restore, a little reloading, and there it is. That is the exact state as Dr. Light left it when he shared it.

I do wanna pause here for a second. This is one of the biggest reasons I am excited about what I'm building here with Shiny State is because at the day job, this is a huge important thing where these applications I mentioned are very complex, often used across different weeks in a design, say, situation and I wanna be able to say, plug in maybe a team member that has domain expertise in what I've done, but I don't wanna make them have to recreate everything I did from scratch. I wanna be able to just share it with them, let them download it, not too dissimilar in the world of cloud where you might share a document via Google Drive or Microsoft OneDrive or whatnot. I think this is kind of the first step to Shiny having that kind of situation here.

I wanna be able to just share it with them, let them download it, not too dissimilar in the world of cloud where you might share a document via Google Drive or Microsoft OneDrive or whatnot. I think this is kind of the first step to Shiny having that kind of situation here.

I'm not gonna pretend I have it all figured out yet, but I feel like this is the first step to enhancing collaborative workflows in these very complex, high-profile situations.

What's next for shinystate

I think this is just the tip of the iceberg for what we could do here with respect to maybe this ecosystem. With the more transparent way of storing these artifacts, there might be some additional custom post-processing we could do if we have an easy way to access where these bookmarkable state files are stored. This could open the door for more traceability, perhaps, both within a user session and perhaps across the users, across the board, if you make clever use of the metadata that's attached to these snapshots. Again, just all possible. Maybe these become inputs to some downstream process for automated reporting. The point is is that I think this is kind of like that key to that safe that I showed earlier with the server-side method, where we've got a lot of things we can do to make this collaboration and this effective use of bookmarkable state even go to the next level.

This is all, just like the rest of the presentations, very early days with this, but I can tell you where we stand now. There is a great set of examples that I baked into the ShinyState package that are included in the package itself. You can run it with Shiny colon colon run example. If you have the name of the example and say package equals ShinyState, you'll get them. There is a very nice package website. I'll have the link on the next slide shortly. With these applications as vignettes, but they're actually embedded in the documentation via, wait for it, WebAssembly and WebR. It is my kind of jam. That's what I talked about last year, and it's been changing my Shiny life, so to speak.

I was really hoping to say it was on CRAN yet, but I'm in the queue. It's not quite done yet, and we'll see what happens, but it's in there in their queuing system. The other thing I wanna work on, definitely in the near future, is compatibility with the popular Shiny kind of orchestration frameworks that have become really powerful in production usage, mainly the Golem framework, which I've been a very big advocate for, as well as the Rhino framework by our colleagues at Epsilon. So that's coming. I just haven't had the time to sift through that yet.

And I wanna go full circle back to what I was feeling earlier in the talk, when you have all these voices kind of coming at you, so to speak. I happen to live in what's called the roundabout capital of the US, Carmel, Indiana, and I view ShinyState as kind of a way, I'm getting stuff still at me in multiple directions, but I feel like ShinyState's that first attempt to kind of put a flow, so to speak, around it, instead of having gridlocks, so to speak, with everybody kind of stopping at once, waiting for me to solve, getting their inputs in right, and getting these sessions up and running, and lacking that collaborative aspect. I think, again, I don't have it all figured out yet, but I think this is definitely the right track, and I'm excited to see where the journey goes.

So that was a whirlwind, but I have links to the documentation site. I got the GitHub repository, as well as these slides. I'm certainly happy to hear from all of you, both for ShinyState, as well as the general R and data science communities. You might know me from my voice. I do a little thing called R Weekly. My co-host, Mike, is here in the audience. We love talking shop about R and Shiny as well. I mean, yeah, fine, connect me elsewhere. Thank you very much.

Q&A

What happens if you try to load an old state, but the UI has since been updated? Does it silently fail, or does it tell the user what has changed? That's a great point. I should have had that in the next steps, is backward compatibility, or at least indicating when it won't work. It will just fail, unfortunately, but that is something on the roadmap, for sure.

This might not be specific to your package, but have you come across anything that isn't bookmarkable? There can be some very complex situations with HTML widgets, so to speak, that have client-side inputs that don't play nicely with a bookmarkable state. Some of that's gotten better, but there have definitely been a handful of widgets that I just cannot bookmark at all, yeah.

So, kind of a follow-up, instead of just HTML widgets, does it work with Shiny modules that are dynamically loaded? So, a Shiny state is definitely module-compatible, so if you can make sure that in your module you flag what to bookmark, or even what not to bookmark, Shiny state respects all that configuration, and there is an example in the docs, in the example apps, one of them is a module-powered app.

And this might be more of a to-do list, does Shiny state come with its own Shiny app to load its own states? Not yet, but that is, again, the examples kind of give you what that could look like, so I could definitely see a separate repo that spins that up eventually, yeah.

And we know you work in pharma, so your answer might be a little bit censored, but could you share one example at work where your collaborator saved and shared states? The app that inspired this, I actually reference very generally, way back in 2018, when I talk about modules, and that app, I can't speak to all of it, but I can say it was helping design a very complicated algorithm for adjustments of multiplicity, and we needed to leverage very complex settings for that search of what is the best answer for it. So that was very much an analysis optimization area, and that is just the tip of the iceberg of what I've done. Now I've done the full gamut of just sharing basic artifacts and whatnot, but that was the one that motivated it.

All right, yeah, and the last one was just another shout-out to Pins from the audience. Thank you very much.