diff --git a/_quarto.yml b/_quarto.yml index 6a555a93..3fc1a956 100644 --- a/_quarto.yml +++ b/_quarto.yml @@ -237,9 +237,10 @@ website: - docs/reactive-foundations.qmd - docs/reactive-patterns.qmd - docs/reactive-mutable.qmd - - section: "Express vs Core" + - section: "Syntax modes" contents: - - docs/express-introduction.qmd + - docs/express-vs-core.qmd + - docs/express-or-core.qmd - docs/express-in-depth.qmd - docs/express-to-core.qmd - section: "Modules" diff --git a/api/core/index.qmd b/api/core/index.qmd index d7b9a2fd..60cef8ab 100644 --- a/api/core/index.qmd +++ b/api/core/index.qmd @@ -2,7 +2,7 @@ This page outlines Shiny _Core_'s API reference. -[Compared to Shiny Express](/docs/express-introduction.qmd), Shiny Core is more structured and verbose, but also more flexible and powerful. +[Compared to Shiny Express](/docs/express-vs-core.qmd), Shiny Core is more structured and verbose, but also more flexible and powerful. Newcomers may want to start with [Shiny Express](../express/index.qmd) Shiny Core API. diff --git a/api/express/index.qmd b/api/express/index.qmd index 81c6759c..ab72fdab 100644 --- a/api/express/index.qmd +++ b/api/express/index.qmd @@ -2,8 +2,8 @@ This page outlines Shiny _Express_'s API reference. -[Compared to Shiny Core](/docs/express-introduction.qmd), Shiny Express is a simpler way to learn and create basic apps, but it is less flexible and powerful. +[Compared to Shiny Core](/docs/express-vs-core.qmd), Shiny Express is a simpler way to learn and create basic apps, but it is less flexible and powerful. -For an introduction to Shiny, see the [Quick Start tutorial](/docs/quick-start.qmd). +For an introduction to Shiny, see the [tutorial](/docs/overview.qmd). {{< include _api_index.qmd >}} diff --git a/api/index.qmd b/api/index.qmd index e382e56e..5c268c9f 100644 --- a/api/index.qmd +++ b/api/index.qmd @@ -2,7 +2,7 @@ This page details the Shiny's full API. New users are encouraged to start from the [Quick Start tutorial](../docs/overview.qmd), and then come back here when you're ready to learn more. -We recommend newcomers start with [Shiny Express](../docs/express-introduction.qmd) instead of the more structured Shiny Core API. +We recommend newcomers start with [Shiny Express](../docs/express-vs-core.qmd) instead of the more structured Shiny Core API. ::: {.panel-tabset .panel-pills .border-0 .p-0 .justify-content-center} diff --git a/docs/comp-r-shiny.qmd b/docs/comp-r-shiny.qmd index d748f5f8..4f11f937 100644 --- a/docs/comp-r-shiny.qmd +++ b/docs/comp-r-shiny.qmd @@ -18,7 +18,7 @@ If you're reading this, we expect that you are an existing R Shiny user with som ::: callout-tip ### Shiny Express -Shiny [express](express-introduction.qmd) is a new, more expressive, way to build PyShiny apps. It is not available in R, so the comparisons drawn below are only relevant to core (i.e., non-express) apps. +Shiny [express](express-vs-core.qmd) is a new, more expressive, way to build PyShiny apps. It is not available in R, so the comparisons drawn below are only relevant to core (i.e., non-express) apps. ::: # Getting started diff --git a/docs/debug.qmd b/docs/debug.qmd index 468a6f6c..f78276ef 100644 --- a/docs/debug.qmd +++ b/docs/debug.qmd @@ -54,7 +54,7 @@ The error displayed in the app is only the final part of the stack trace, but th ::: callout-note ## Sanitized error messages -When Shiny apps are deployed, error messages are sanitized to the eliminate the possibility of leaking sensitive information. To unsanitize error messages, you'll need to set `sanitize_errors=True` in the `App` constructor (of a [Shiny core app](express-introduction.qmd)). +When Shiny apps are deployed, error messages are sanitized to the eliminate the possibility of leaking sensitive information. To unsanitize error messages, you'll need to set `sanitize_errors=True` in the `App` constructor (of a [Shiny core app](express-vs-core.qmd)). ::: diff --git a/docs/express-in-depth.qmd b/docs/express-in-depth.qmd index 300cf35d..f1ab572a 100644 --- a/docs/express-in-depth.qmd +++ b/docs/express-in-depth.qmd @@ -5,202 +5,308 @@ editor: wrap: sentence --- -This article digs into some more advanced topics in Shiny Express. -It also highlights how some of these topics are less of a concern in Shiny Core. +Shiny Express has a simple syntax that makes it easy to get started. +But achieving this outer simplicity requires some inner complexity. +As your usage of Shiny Express becomes more advanced, you may start to encounter some of this complexity. -## Holding +(In comparison, Shiny Core requires slightly more effort to learn and to write, but is more predictable and easier to reason about.) -One way to avoid repeating yourself in Express is to use the `ui.hold()` context manager. +This article peels back the curtain on Shiny Express, and reveals some of the hurdles you may run into as your apps grow. +Where possible, we've added utilities and techniques to deal with these issues. -```python -with ui.hold() as hello_card: - with ui.card(): - "Hello world!" +It's our hope that after reading this article, you'll have a far more complete mental model of how Shiny Express works, and be able to write more advanced apps with less friction. +That being said, if you spend a lot of time using these advanced Express features, you may want to consider switching to Shiny Core. + +The following information is organized into two broad topics: [Programming UI](#programming-ui) and [Shared objects](#shared-objects). + +## Programming UI + +Let's start with an unremarkable bit of Shiny Express UI code: one card container, with a heading tag and a string inside. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: horizontal +from shiny.express import ui -hello_card -hello_card +with ui.card(class_="mt-3"): + ui.h3("Socrates") + "470-399 BC" ``` -::: callout-note -### Parameterized reuse +Now let's say we want to add a second card. -As we'll see later, `@expressify` allows facilitates reuse of Express code, but in a parameterized way. -::: +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: horizontal +from shiny.express import ui -`ui.hold()` is also useful for decoupling UI and server logic. -To understand why decoupling is useful, let's first look at how Express normally couples UI and server logic. +with ui.card(class_="mt-3"): + ui.h3("Socrates") + "470-399 BC" -## Decoupling {#decoupling} +with ui.card(class_="mt-3"): + ui.h3("Immanuel Kant") + "1724-1804" +``` -In Express, things like render functions get displayed exactly where they appear in the code. -This is great for simple apps, but can lead to code that's harder to reason about as your application grows in size and complexity. -For example, consider this snippet of an Express app -- can you visualize, at a glance, how the overall UI is structured? +That works. +But as good programmers, we don't like to repeat ourselves. +So we'll follow programming best practices and refactor that UI logic into a function: -
- Hide code -```{.python} -with ui.layout_columns(): - with ui.card(): - @render.plot - def pair_plot(): - df = load_penguins() - if df is None: - print("Dataframe is empty") - return - - # Drop rows with missing values - df = df.dropna() - - # Get list of features - features = df.select_dtypes(include=[np.number]).columns.tolist() - - # Create a figure and axes with a subplot for each pair of features - fig, axs = plt.subplots(len(features), len(features), figsize=(15, 15)) - - # Create scatter plots for each pair of features - for i in range(len(features)): - for j in range(len(features)): - if i != j: - for species in df['species'].unique(): - axs[i, j].scatter(df[df['species']==species][features[i]], - df[df['species']==species][features[j]], - label=species) - axs[i, j].set_xlabel(features[i]) - axs[i, j].set_ylabel(features[j]) - else: - axs[i, j].text(0.5, 0.5, features[i], ha='center', va='center') - - # Add a legend - handles, labels = axs[0, 1].get_legend_handles_labels() - fig.legend(handles, labels, loc='upper center') - - fig.tight_layout() - return fig - ui.input_select("species", "Species", ["Chinstrap", "Adelie", "Gentoo"]) +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: horizontal +from shiny.express import ui + +def person(name, years): + with ui.card(class_="mt-3"): + ui.h3(name) + years + +person("Socrates", "470-399 BC") +person("Immanuel Kant", "1724-1804") ``` -
-The `render.plot` function above has a lot going on, and so it's easy to overlook that there's a `ui.input_select()` which appears just below the plot, inside the same card. +Uh oh, that doesn't look right. +Such a simple and obviously correct refactor, yet the cards are now empty! -Express does offer a way to workaround this problem: the `ui.hold()` context manager, which allows you to define a `render` function in one place, then display it in another. -As a result, it's a lot more clear how the overall UI is structured: +### Interactive mode vs script mode -```{.python} -with ui.hold(): - @render.plot - def pair_plot(): - ... # code here +To understand why, you first need to know that the Python interpreter has two different ways of executing code: _interactive_ mode and _script_ mode. + +If you've been using Python for a while, you intuitively understand these modes, even if you've never stopped to think about it. +If you run `python` and type `"hello"` into the prompt, you'll see `hello` printed back to you. +But if you create a `script.py` file containing `"hello"` and run `python script.py`, you won't see anything printed. +In interactive mode, the Python interpreter automatically prints the result of each expression; in script mode, `print()` must be called explicitly. -# Overall UI structure is now more clear -with ui.layout_columns(): - with ui.card(): - ui.output_plot("pair_plot") - ui.input_select("species", "Species", ["Chinstrap", "Adelie", "Gentoo"]) +Shiny Express executes your `app.py` file in interactive mode, not script mode. +Even though you're not at an interactive prompt, it still "prints" the result of each expression. +Now, it doesn't literally use the `print()` function---that would just print text to the console---but a lower-level function in Python called [`sys.displayhook`](https://docs.python.org/3/library/sys.html#sys.displayhook) that is designed to be overridden by frameworks like Shiny (and Jupyter, incidentally). + +This is so important that we'll repeat it: **Shiny Express executes your `app.py` file in interactive mode, which automatically calls `sys.displayhook()` on each expression.** + +That's why, in our simple examples above, a bare string like `"470-399 BC"` gets printed to the screen. +If Shiny Express was executed in script mode (like Shiny Core is, by the way), you'd have to rewrite it as: +```python +sys.displayhook("470-399 BC") ``` +to get the string to appear in the UI. +Gross. -::: callout-note -### Decoupling in Shiny Core +### Functions in interactive mode -Shiny Core enforces decoupling of UI and server in a similar way. -::: +One important aspect of interactive mode is that only top-level expressions are printed. +If you define a function in interactive mode, the expressions that make it up are not automatically printed. + +```python +>>> def foo(): +... "470-399 BC" +... +>>> foo() +>>> +``` +Now that you understand that Shiny Express executes in interactive mode, you can see why our `person()` function doesn't work. +The UI code in the body of the `person()` function isn't automatically printed because it's not at the top level. -## Implicit UI +You could fix this by calling `sys.displayhook` on each UI element. -In [decoupling](#decoupling), we first saw how decoupling server from UI logic requires a `ui.output_*()` container element connect the two. -Power users will find that having explicit control over these containers gives them more control over those component styling (since [UI is HTML](ui-html.qmd), HTML/CSS can be used to customise the component containers). +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: horizontal +import sys +from shiny.express import ui -Express has one other important place where an implicit UI container is used: the overall page layout. -This is often convenient, since Express can infer a sensible layout based on the top-level UI components, but it can also be limiting to not have explicit control over the page layout. -Express does offer a `ui.page_opts()` to add a title and other page options, but it's not as flexible working directly with an explicit page container. +def person(name, years): + with ui.card(class_="mt-3"): + sys.displayhook(ui.h3(name)) + sys.displayhook(years) +person("Socrates", "470-399 BC") +person("Immanuel Kant", "1724-1804") +``` -## Reusable abstractions {#expressify} +OK, it works, but that's pretty gross. +Is there a better way to fix this problem? -In [holding](#holding), we learned how `ui.hold()` helps to avoid repeating Express code verbatim. -This is great, but it doesn't allow for code parameterization, which is useful for creating reusable abstractions that manage complexity. -For example, suppose we want to create many cards that each contain a plot and a title. -Instead of repeating card logic over and over, we can wrap it in a function decorated with `@expressify`. +The answer is yes, but before we get to that, let's take a step back and restate what we've learned so far. + +* You can call `sys.displayhook()` to tell Shiny Express to display something. +* Shiny Express executes `app.py` in interactive mode, not script mode. +* In interactive mode, only top-level expressions are displayed, not expressions in function bodies. + +Now let's see where this approach causes problems, and how we can solve them. +We'll start with the `person()` function we just tried to write. + +### Problem: Writing UI generating functions + +We want to write functions that generate UI, and we don't want to have to call `sys.displayhook()` by hand. + +#### Solution: `@expressify` decorator + +Apply the `@expressify` decorator to a function to tell Shiny Express that the function body should be executed in interactive mode. +Think of it as rewriting the function body so that `sys.displayhook()` wraps every expression. ```{shinylive-python} #| standalone: true #| components: [editor, viewer] -#| layout: vertical -#| viewerHeight: 300 -import numpy as np -import matplotlib.pyplot as plt -from shiny.express import expressify, output, render, ui +#| layout: horizontal +from shiny.express import expressify, ui @expressify -def custom_card(i): - with ui.card(): - f"Card {i}" - - @output(id=f"hist_{i}") - @render.plot(alt="A histogram", height=150) - def _(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) - plt.hist(x, 20, density=True) - -for i in range(3): - custom_card(i) +def person(name, years): + with ui.card(class_="mt-3"): + ui.h3(name) + years + +person("Socrates", "470-399 BC") +person("Immanuel Kant", "1724-1804") ``` ::: callout-note -### Shiny Core embraces reusable functions - -Since Shiny Core embraces functional programming, creating reusable code is as simple as defining a function. -Reusable abstractions in Shiny Core are often simpler to understand since it's easier to see/remember just the inputs and output of a function, rather its implementation details. +### Shiny Core perspective +Shiny Core doesn't need an `@expressify` decorator because it does not rely on interactive mode and never calls `sys.displayhook` anyway. +Instead, UI functions are just normal functions that happen to return UI objects. ::: +### Problem: Collect UI code into a variable + +Sometimes we have a need to generate UI for some purpose other than directly displaying it. +For example, we might want to save it to be displayed later, or multiple times. + +This works OK for simple objects like strings (naturally) and even non-container UI elements---you can simply store them as variables, and that works. +But in the examples above, we're using `with ui.card():`, and you can't store a `with` statement in a variable. -## Reactive Express code {#reactive-displays} +```python +>>> x = with ui.card(): + File "", line 1 + x = with ui.card(): + ^^^^ +SyntaxError: invalid syntax +``` + +You also cannot use `with ui.card() as x:` syntax, because UI context managers like `ui.card()` don't yield anything, for reasons we'll get to in a moment. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: horizontal +from shiny.express import expressify, ui + +with ui.card(class_="mt-3") as x: + ui.h3("Socrates") + "470-399 BC" -In Shiny, just `render` and `reactive` functions are reactive. -That is, other UI components like `ui.input_*()`, `ui.card()`, etc. are static: they don't change once the app is rendered. -In order to make those otherwise static components reactive in Express, you have to wrap them in a function decorated with `@render.express`. -This decorator is a bit of an exception to the rule compared to other `render` decorators since it's called for its side-effects, and not for its return value. +x +x +x +``` -For example, suppose we want a checkbox to toggle whether to display even or odd numbers. -We can do this by wrapping the Express code in a function and decorating it with `@render.express`. -As a result, you can read reactive dependencies (e.g., `input.even()`), and function will be re-run whenever those dependencies change. +It looks for a moment like it worked, but no, it didn't; instead of displaying the card three times, it displayed it once. +That's because leaving the `with ui.card():` context immediately displays the entire card, and then the `x` is just assigned a `None` value, which doesn't display anything. + +#### Solution: `ui.hold()` context manager + +The `ui.hold()` context manager allows you to collect UI code into a variable. ```{shinylive-python} #| standalone: true #| components: [editor, viewer] -#| layout: vertical -#| viewerHeight: 300 -import numpy as np -import matplotlib.pyplot as plt -from shiny.express import input, expressify, output, render, ui +#| layout: horizontal +from shiny.express import expressify, ui + +with ui.hold() as x: + with ui.card(class_="mt-3"): + ui.h3("Socrates") + "470-399 BC" + +x +x +x +``` + +In this case, it's just a single card, but there's no limit to how much or how little UI you can nest under `ui.hold()`. + +::: callout-note +### Shiny Core perspective +In Shiny Core, UI objects are just normal objects, so you can assign them to variables no differently than you would an integer or a list. +::: + +### Problem: Reactively rendering UI + +So far, all of the UI we've generated has been "static"---it's generated once, when the page loads, and never changes. +It's pretty common in Shiny to want to generate UI in response to user input or server events. + +We can do this in Shiny Express by using the `@render.ui` decorator, which expects a function that returns a UI object. +We can combine `@expressify` and `ui.hold()` to make this work. +(Spoiler alert: we're just setting up a strawman solution here, we'll get to the "right" way in a moment.) + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: horizontal +from shiny.express import expressify, input, render, ui +ui.input_text("name", "Name", "Socrates") +ui.input_text("years", "Years", "470-399 BC") + +@render.ui @expressify -def custom_card(i): - with ui.card(): - f"Card {i}" +def person(): + with ui.hold() as result: + with ui.card(class_="mt-3"): + ui.h3(input.name()) + input.years() + return result +``` + +That does work; change the name or year inputs, and the card updates. +But it's way more boilerplate than we'd like. - @output(id=f"hist_{i}") - @render.plot(alt="A histogram", height=150) - def _(): - np.random.seed(19680801) - x = 100 + 15 * np.random.randn(437) - plt.hist(x, 20, density=True) +#### Solution: `@render.express` decorator +The `@render.express` decorator is a shorthand for that combination of `@render.ui` + `@expressify` + `ui.hold`. +You can just think of it as "reactively render a chunk of Express code". + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: horizontal +from shiny.express import expressify, input, render, ui + +ui.input_text("name", "Name", "Socrates") +ui.input_text("years", "Years", "470-399 BC") @render.express -def cards(): - for i in range(input.n()): - custom_card(i) +def person(): + with ui.card(class_="mt-3"): + ui.h3(input.name()) + input.years() ``` +It's almost anticlimactically simple to use, considering how much explaining we had to do to get here. +::: callout-note +### Shiny Core perspective +In Shiny Core, you should use `@render.ui` and skip `@expressify` or `ui.hold()`---they're not needed. Instead, your render function would return a UI object directly. +::: + +### Summary + +* When writing a function that contains Shiny Express UI code, always decorate it with `@expressify`. This tells Python to execute the function body in interactive mode, which is necessary for the UI to be displayed. +* If you want to collect UI into a variable instead of displaying it, wrap it in a `with ui.hold() as var_name:` block. +* If you want to reactively render UI, decorate the function with `@render.express`. ## Shared objects For better performance, it's often useful to have some code run _once_ when the app initializes, not every time a new connection (i.e., session) is made. -Normal Express code is re-executed everytime a new connection is made, so it's not a good place to do expensive work that only needs to be done once. +All of the code in a Shiny Express `app.py` file is re-executed every time a new connection is made, so it's not a good place to do expensive work that only needs to be done once. + Fortunately, if you move expensive code to a separate module, it will only be executed once (and objects can then be shared across sessions). ```{shinylive-python} @@ -229,11 +335,13 @@ col1,col2 ::: callout-note +### Shiny Core perspective In Shiny Core, code outside of the `server` function scope runs once per startup (not per user session). See the code below for the equivalent Shiny Core app.
- Show code +Show code + ```{.python} from shiny import App, render, ui import pandas as pd @@ -261,7 +369,7 @@ It's also possible to share reactive objects across sessions. This can be potentially dangerous since one users activity could impact another's, but also quite useful in combination [`reactive.file_reader`](../api/reactive.file_reader.qmd) and [`reactive.poll`](../api/reactive.poll.qmd) to create a reactive data source that's only polled once, no matter how many users are connected. ::: -## Sessions +### Sessions Shiny apps have an object that represent a particular user's [session](../api/Session.html). This object is useful for a variety of more advanced tasks like [sending messages to the client](../api/Session.html#shiny.Session.send_custom_message) and [serving up session-specific data](../api/Session.html#shiny.Session.dynamic_route). diff --git a/docs/express-introduction.qmd b/docs/express-introduction.qmd deleted file mode 100644 index f424b7e4..00000000 --- a/docs/express-introduction.qmd +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: Introduction -editor: - markdown: - wrap: sentence ---- - -Up until now, these docs have focused on Shiny _Express_, which compared to Shiny _Core_, offers a simpler way to learn and write basic apps. -The simplicity that Express offers, however, can come at the cost of reduced flexibility and maintainability. -In other words, Express is quicker to get started with, but can eventually lead to code that's more difficult for others (or your future self) to read, maintain, and extend. -Fortunately, it's straightforward to [transition](express-to-core.qmd) from Express to Core since many of the foundations (i.e., [components](ui-overview.qmd), [reactivity](reactive-foundations.qmd), etc.) are the same. - -Compared to Express, Core enforces a more structured approach that requires more initial effort to use. -However, we believe that investment pays off on projects where increased flexibility and maintainability are important. -This is because Shiny Core enforces: - -1. **Decoupling of UI and server**. Decoupling forces you to separate concerns in such a way that it's easier refactor and reason about a large amount of code. -2. **Functional programming**. Express encourages a more imperative (less functional) style, which can be easier to write, but also harder to read. This is especially true when you need to create [reusable extractions](express-in-depth.qmd##expressify). -3. **More explicit UI**. Express has magical defaults that make things simpler, but can also mask what's actually going on behind-the-scenes, and offers less opportunities to customize behavior. - -The next article illustrates how these downsides can be mitigated to some extent in Express with some advanced tooling. -However, if you feel yourself reaching for these tools, it might be time to switch to Core. -If you're already convinced that Shiny Core is right for your app, feel free to skip ahead to the [transitioning](express-to-core.qmd) article. - -::: callout-note -### Shiny Core is a superset of Express - -Shiny Core is a superset of Express, meaning that anything you can do in Express, you can also do in Core. -The reverse is not true, however. -For example [Shiny Modules](modules.qmd) are not currently supported in Express. -::: - -::: callout-note -### Shiny Core is more mature - -Shiny Express is still relatively new, and is still being actively developed. -As such, you can expect the Express experience to keep improving over time. -::: diff --git a/docs/express-or-core.qmd b/docs/express-or-core.qmd new file mode 100644 index 00000000..ca2fe836 --- /dev/null +++ b/docs/express-or-core.qmd @@ -0,0 +1,267 @@ +--- +title: Choosing a syntax +editor: + markdown: + wrap: sentence +--- + +Now that you are familiar with [the differences between Shiny Express and Shiny Core](express-vs-core.qmd), you might be wondering how to choose between them. + +In this article, we'll suggest some guidelines, but it's important to note that there are not many hard and fast rules. +There is a lot of overlap between the capabilities of the two syntaxes, so feel free to choose whichever feels more natural and comfortable for you. + +Shiny Express is designed to get you up and running as quickly as possible. +It's also designed to let you author your app with a minimum of boilerplate. +As a result, it really shines in the early stages of both the learning journey and the lifecycle of an app. + +Compared to Express, Core enforces a more structured approach that requires more initial effort to use. +However, that investment has its own payoff. + +The bulk of the rest of this article will focus on the advantages of Shiny Core. +This isn't because we think it's better, but because Shiny Express's advantages---being more approachable and more concise---are fairly self-evident, while Shiny Core's advantages are more subtle. + +## Maintainability + +**Consider using Shiny Core if you are building a large or long-lived app.** + +The most important difference between the two syntaxes is that Express allows you to intermingle UI and server code, while Core requires you to separate them. +The separation that Core requires can feel inconvenient while adding features to your app, as each new output requires you to edit two different places in your `app.py` file. + +But for larger and longer-lived apps, Shiny Core's more opinionated approach becomes an advantage. +It is much easier to add, remove, or relocate pieces of your UI when all of its code is in one place, with no server code to confuse things. Similarly, when you're trying to understand the relationship between a reactive calculation and some outputs, it's much easier to do so when you don't have intermingled UI code in the way. + +At the [bottom of this page](#appendix), you'll find a Shiny Core version of the [dashboard app from the Essentials section](user-interfaces.qmd#all-together-now){target="_blank"} of this guide. +Compare their respective source code, and consider: + +* Which version makes the structure of the UI more obvious? +* Which version would make you more confident in moving UI elements around? +* Which version makes it easier to understand the reactive calcs and outputs? + +## Feature set + +**Consider using Shiny Core if you need to use Shiny Modules or dynamic UI.** + +At this time, Shiny Core's functionality is a superset of Express, meaning that anything you can do in Express, you can also do in Core. +The reverse is not true, however. + +Most importantly, [Shiny Modules](modules.qmd) are supported in Shiny Core but not (yet) in Shiny Express. Shiny Modules are extremely useful for organizing large apps into smaller, more manageable pieces, and are also a mechanism for reusing Shiny application logic. + +Shiny Core also has `ui.insert_ui()` and `ui.remove_ui()` functions, which is a way to imperatively add or remove UI elements from the app at any time. +Despite being available in `shiny.express.ui`, these functions do not currently work well with Shiny Express. The same goes for `ui.modal_show()`. + +## Maturity + +**Consider using Shiny Core if you care more about maturity and stability than convenience.** + +Given its longer history, Shiny Core is naturally more mature than Shiny Express in both syntax and implementation. + +We've carefully designed the Shiny Express syntax, and hope not to have to make breaking changes to it. +However, we don't know what we don't know, and it's possible that user feedback or our own testing will someday require us to make significant changes. + +Similarly, we are constantly testing Shiny Express, but as of this writing, it has not has as much real-world use as Shiny Core. +Therefore, with Shiny Core, you are less likely to encounter bugs. + +## Familiarity to R users + +**Consider using Shiny Core if you are an R user who is already familiar with Shiny.** + +While Shiny Core is not a literal translation of Shiny for R, it is much closer to it than Shiny Express. +The UI/server separation, the nested UI function calls, the matching of output IDs to render functions, are all going to feel very natural to experienced Shiny for R app authors. + + + +## Appendix + +The following is the dashboard application from the Essentials section of this guide, rewritten using Shiny Core. +Compare it to [the original](user-interfaces.qmd#all-together-now). + +::: {.column-page-right} + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +#| viewerHeight: 800 +import faicons as fa +import plotly.express as px +from shinywidgets import output_widget, render_plotly + +from shiny import App, reactive, render, req, ui + +# Load data and compute static values +tips = px.data.tips() +bill_rng = (min(tips.total_bill), max(tips.total_bill)) + +ICONS = { + "user": fa.icon_svg("user", "regular"), + "wallet": fa.icon_svg("wallet"), + "currency-dollar": fa.icon_svg("dollar-sign"), + "gear": fa.icon_svg("gear") +} + +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_slider("total_bill", "Bill amount", min=bill_rng[0], max=bill_rng[1], value=bill_rng, pre="$"), + ui.input_checkbox_group("time", "Food service", ["Lunch", "Dinner"], selected=["Lunch", "Dinner"], inline=True), + ui.input_action_button("reset", "Reset filter"), + ), + ui.layout_columns( + ui.value_box( + "Total tippers", + ui.output_ui("total_tippers"), + showcase=ICONS["user"], + showcase_layout="left center", + ), + ui.value_box( + "Average tip", + ui.output_ui("average_tip"), + showcase=ICONS["wallet"], + showcase_layout="left center", + ), + ui.value_box( + "Average bill", + ui.output_ui("average_bill"), + showcase=ICONS["currency-dollar"], + showcase_layout="left center", + ), + fill=False, + ), + ui.layout_columns( + ui.card( + ui.card_header("Tips data"), + ui.output_data_frame("table"), + full_screen=True, + ), + ui.card( + ui.card_header( + "Total bill vs tip", + ui.popover( + ICONS["gear"], + ui.input_radio_buttons( + "scatter_color", None, + ["none", "sex", "smoker", "day", "time"], + inline=True, + ), + title="Add a color variable", + placement="top", + ), + class_="d-flex justify-content-between align-items-center" + ), + output_widget("scatterplot"), + full_screen=True, + ), + ui.card( + ui.card_header( + "Tip percentages", + ui.popover( + ICONS["gear"], + ui.input_radio_buttons( + "tip_perc_y", "Split by:", + ["sex", "smoker", "day", "time"], + selected="day", + inline=True, + ), + title="Add a color variable", + ), + class_="d-flex justify-content-between align-items-center", + ), + output_widget("tip_perc"), + full_screen=True, + ), + col_widths=[6, 6, 12], + ), + title="Restaurant tipping", + fillable=True, +) + +def server(input, output, session): + + # -------------------------------------------------------- + # Reactive calculations and effects + # -------------------------------------------------------- + + @reactive.calc + def tips_data(): + bill = input.total_bill() + idx1 = tips.total_bill.between(bill[0], bill[1]) + idx2 = tips.time.isin(input.time()) + return tips[idx1 & idx2] + + @reactive.effect + @reactive.event(input.reset) + def _(): + ui.update_slider("total_bill", value=bill_rng) + ui.update_checkbox_group("time", selected=["Lunch", "Dinner"]) + + # -------------------------------------------------------- + # Outputs + # -------------------------------------------------------- + + @render.ui + def total_tippers(): + return tips_data().shape[0] + + @render.ui + def average_tip(): + d = tips_data() + req(d.shape[0] > 0) + perc = d.tip / d.total_bill + return f"{perc.mean():.1%}" + + @render.ui + def average_bill(): + d = tips_data() + req(d.shape[0] > 0) + bill = d.total_bill.mean() + return f"${bill:.2f}" + + @render.data_frame + def table(): + return render.DataGrid(tips_data()) + + + @render_plotly + def scatterplot(): + color = input.scatter_color() + return px.scatter( + tips_data(), + x="total_bill", + y="tip", + color=None if color == "none" else color, + trendline="lowess" + ) + + @render_plotly + def tip_perc(): + from ridgeplot import ridgeplot + dat = tips_data().copy() + dat.loc[:, "percent"] = dat.tip / dat.total_bill + yvar = input.tip_perc_y() + uvals = dat[yvar].unique() + + samples = [ + [ dat.percent[dat[yvar] == val] ] + for val in uvals + ] + + plt = ridgeplot( + samples=samples, labels=uvals, bandwidth=0.01, + colorscale="viridis", colormode="row-index" + ) + + plt.update_layout( + legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5) + ) + + return plt + +app = App(app_ui, server) + +## file: requirements.txt +ridgeplot +``` + +::: diff --git a/docs/express-vs-core.qmd b/docs/express-vs-core.qmd new file mode 100644 index 00000000..062ad6ad --- /dev/null +++ b/docs/express-vs-core.qmd @@ -0,0 +1,262 @@ +--- +title: Express vs. Core +editor: + markdown: + wrap: sentence +--- + +Shiny is one framework with two different syntax options: _Shiny Express_ and _Shiny Core_. +Up until now, these docs have focused on Shiny Express. +In this section, we'll dig into the differences between the two, and why you might choose one or the other. + +Don't worry, though. +There is an enormous amount of overlap between the two, and the vast majority of apps will be well served by either Shiny Express or Shiny Core. + +And if you do decide to switch, the process is relatively straightforward---especially in the direction of Express to Core, as the latter supports a superset of the former's capabilities. We go into more detail on this in the [transitioning](express-to-core.qmd) article. + +## Background + +Shiny for Python was unveiled in 2022 with a single syntax option, which we now call Shiny Core. +It drew inspiration from the [Shiny framework for R](https://shiny.posit.co/), which has been around for over a decade and is used by hundreds of thousands of data scientists around the world. +Our goal was to make Shiny Core feel Pythonic, as opposed to a literal port of Shiny for R, but carry over the same core principles and tradeoffs. + +In January 2024, we introduced Shiny Express as a second syntax option. +Express is built on top of Core, and is designed to be extremely easy to write, while preserving most of the power and flexibility of Shiny. + +## Differences between Express and Core + +The major differences between Shiny Express and Core are the following: + +- Slightly different import statements +- Different organization of UI and server code +- Implicit vs. explicit placement of outputs +- Different syntax for UI containers + +Let's examine each of these in more detail. + +### Import statements + +A Shiny Core app file usually contains an import statement like: + +```python +from shiny import App, reactive, render, ui +``` + +In Shiny Express, you'll instead see: + +```python +from shiny import reactive +from shiny.express import input, render, ui +``` + +Note that both import `ui` and `render`, but from different places. +While Express's `shiny.express.ui` has almost all of the same UI functions as Core's `shiny.ui`, their function signatures often differ slightly, to reflect Express's different usage patterns. +And the `render` functions---well actually, they are identical right now, but we're planning to add some Express-specific features to the `shiny.express.render` versions in the near future. + +Meanwhile, the `reactive` module is unchanged between Core and Express, as the two modes share the same reactive foundations. + +Finally, notice that Express also imports `input` from `shiny.express`. +This isn't needed in Core, because the `input` object is passed into the server function as an argument. +Since Express has no server function, we made it an attribute of `shiny.express`. + +### Organization of UI and server code + +**Every Shiny Core app file has a UI section, and a server section;** the two are separate and distinct. + +The UI section consists of a single (potentially very long and deeply nested) expression, stored as a variable named `app_ui` by convention. +The object this produces is actually straight up HTML, which is sent to the browser when it first loads the app. + +The server section is a function, named `server` by convention, that always takes the arguments `input`, `output`, and `session`. +This function contains render functions and reactive functions, which are used to update the UI in response to user input. + +You can think of the UI section as a template, with `ui.output_xx()` calls as placeholders for outputs, and the server section as the instructions for how to populate those outputs. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +# Core +from shiny import ui, render, reactive, App +from datetime import datetime + +app_ui = ui.page_fixed( + ui.h1("Title"), + ui.output_code("greeting"), +) + +def server(input, output, session): + @reactive.Calc + def time(): + reactive.invalidate_later(1) + return datetime.now() + + @render.code + def greeting(): + return f"Hello, world!\nIt's currently {time()}." + +app = App(app_ui, server) +``` + +**In Shiny Express, there isn't this hard distinction** between UI and server. + +Instead, everything coexists as top-level code in the app file: input components, layout directives, outputs (including their rendering logic), and reactive functions. + +Also, you aren't forced to combine your UI into a single complex object. +You can use multiple UI objects, and they will be combined together by the framework. + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +# Express +from shiny import ui, render, reactive, App +import shiny.express +from datetime import datetime + +ui.h1("Title") + +@reactive.Calc +def time(): + reactive.invalidate_later(1) + return datetime.now() + +@render.code +def greeting(): + return f"Hello, world!\nIt's currently {time()}." +``` + +Again, notice how `greeting` in this app does _not_ have a corresponding call to `output_code("greeting")`. +This is because in Shiny Express, `render` functions automatically add an output to the page---no need to do it manually. + +##### Core advantages + +- Because the UI structure is kept separate from the server, it is easier to read, reorder, and restructure. This advantage grows as app UIs grow larger. +- Explicit server function declaration gives us a natural place to put code that should only execute at startup (top level) versus for each session (server function body). In contrast, in Express, all of the code in the app file is executed for each session. + +##### Express advantages + +- It's nice for beginners not to have to learn about the difference between UI and server. +- Avoids having to write code in two different places for a single output, and having to make the IDs match up. +- No need to write nested function declarations (i.e. functions inside the server function), which can be surprising to Python programmers. + +### Implicit vs. explicit placement of outputs + +For an output to appear in a Shiny app of any type, the framework needs to know two things: where it should go in the UI, and how it should be rendered. + +In Shiny Core, this is done in two separate steps. In the UI, you create a placeholder for the output, using a function like `ui.output_plot("plot1")`. Then, in the server, you create a rendering function, using a decorator like `@render.plot`, and name the function the same as the placeholder, like `def plot1():`. + +In Shiny Express, this is done in a single step. You create a rendering function, using a decorator like `@render.plot`, and name the function however you like (as long as it's unique). The framework automatically creates a placeholder in the UI where the function is defined. + +##### Core advantages + +- Moving an output from one place in the UI to another is as simple as moving the corresponding placeholder function. +- The `ui.output_xx()` functions currently have some additional arguments that allow you to customize the output's behavior, like enabling plot brushing (with `ui.output_plot("plot1", brush=True)`). This is not as natural in Express, though workarounds do exist---see the [next article](express-in-depth.qmd). + +##### Express advantages + +- No need to carefully match the names of placeholders and rendering functions. + +### Different syntax for UI containers + +Broadly speaking, there are two kinds of UI components in Shiny: _container components_, which, as the name suggests, can contain other components, and _non-container components_, which cannot. +(You can also think of the UI as a tree data structure: container components have children, while non-container components are leaf nodes in the tree.) + +Here are some examples of container components: + +- [`ui.sidebar()`](../layouts/sidebars.qmd) +- [`ui.card()`](../layouts/panels-cards.qmd#content-divided-by-cards) +- [`ui.layout_columns()`](../layouts/arrange.qmd#grid-layouts) +- `ui.div()` + +Here are some examples of non-container components: + +- [`ui.input_text()`](../components/inputs/text-box.qmd) +- [`ui.output_plot()`](../components/outputs/plot-matplotlib.qmd) + +**In Shiny Core, to put a component inside of a container, you nest the function calls,** like `ui.sidebar(ui.input_text())`. + +::: {.column-page-right} +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +# Core +from shiny import ui, render, App + +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_text("txt_in", "Type something here:"), + open="always", + ), + ui.card( + ui.output_code("result"), + ) +) + +def server(input, output, session): + @render.code + def result(): + return f"You entered '{input.txt_in()}'." + +app = App(app_ui, server) +``` +::: + +By contrast, **in Shiny Express, container components like `ui.sidebar()` are _context managers_**, and used via `with` statements. +Their child components go within the `with` block. + +::: {.column-page-right} +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +# Express +from shiny import render, App +from shiny.express import input, ui + +with ui.sidebar(): + ui.input_text("txt_in", "Type something here:") + +with ui.card(): + @render.code + def result(): + return f"You entered '{input.txt_in()}'." +``` +::: + +##### Core advantages + +- Passing children as arguments is beautifully simple and robust from a programming perspective. +- Containers and non-containers behave very similarly (in fact, non-containers act no different than containers with no children). +- All of the Core UI components are simple value-returning functions, without side effects. This makes them easy to compose, reuse, refactor, inspect, and test. + +##### Express advantages + +- The `with` block syntax is slightly more readable, as it depends only on indentation and not on parentheses and commas. +- You can put arbitrary Python code inside `with` blocks, including loops, conditionals, import statements, function definitions, reactive functions, and output rendering functions. (In Core, you are limited to Python expressions that generate UI.) + +::: {.callout-note} +In unusual situations, you might want to create HTML content that doesn't use context managers. HTML tag functions, like `div()` and `span()` can actually be used as context managers or as regular functions, so the following are equivalent: + +```{shinylive-python} +#| standalone: true +#| components: [editor, viewer] +#| layout: vertical +# Express +from shiny.express import ui + +with ui.div(): + with ui.pre(style="background-color: #eff;"): + "Hello!" + +ui.div( + ui.pre( + "Hello!", + style="background-color: #eff;", + ), +) +``` + +More complex component functions, such as `ui.sidebar()` and `ui.card()`, can only be used as context managers in Shiny Express. + +::: diff --git a/docs/modules.qmd b/docs/modules.qmd index 77ea20bc..e0cdbfb8 100644 --- a/docs/modules.qmd +++ b/docs/modules.qmd @@ -20,7 +20,7 @@ Python modules are a generic way to organize objects in a namespace, while Shiny ::: callout-warning ### Shiny core only -Modules are not currently supported in [Shiny Express](express-introduction.qmd) apps. +Modules are not currently supported in [Shiny Express](express-vs-core.qmd) apps. ::: diff --git a/docs/overview.qmd b/docs/overview.qmd index 82c8313c..74180a8b 100644 --- a/docs/overview.qmd +++ b/docs/overview.qmd @@ -50,7 +50,7 @@ This example demonstrates the basic mechanics behind Shiny apps: * Outputs are created by decorating a function with `@render.*`. * Inside a `render` function, `input` values can be read [reactively](#reactivity). * When those `input` values change, Shiny knows how to minimally re-render output. -* This example happens to use `shiny.express` which, [compared to core Shiny](express-introduction.qmd), reduces the amount of code required. +* This example happens to use `shiny.express` which, [compared to core Shiny](express-vs-core.qmd), reduces the amount of code required. ### Components {#components} diff --git a/docs/ui-customize.qmd b/docs/ui-customize.qmd index f3864da5..9d38c3e1 100644 --- a/docs/ui-customize.qmd +++ b/docs/ui-customize.qmd @@ -224,7 +224,7 @@ app = App(app_ui, None, static_assets=app_dir / "www") ``` ::: callout-note -Serving local files is currently only supported with [Shiny Core](express-introduction.qmd) (i.e., it doesn't yet work with Express). +Serving local files is currently only supported with [Shiny Core](express-vs-core.qmd) (i.e., it doesn't yet work with Express). ::: diff --git a/docs/ui-overview.qmd b/docs/ui-overview.qmd index 0fbc3c6b..97c08678 100644 --- a/docs/ui-overview.qmd +++ b/docs/ui-overview.qmd @@ -203,8 +203,8 @@ See the [layout gallery](/layouts) for an overview of available layout mechanism ### Page layouts A special type of layout is the page layout, which is used to start a new UI. -In [Shiny Express](express-introduction.qmd), the page layout is implicit, and automatically inferred from the top-level UI components. -In [Shiny Core](express-introduction.qmd), the page layout is explicit, meaning that the UI starts with a page layout component (e.g. `ui.page_fluid()`, `ui.page_sidebar()`, etc). +In [Shiny Express](express-vs-core.qmd), the page layout is implicit, and automatically inferred from the top-level UI components. +In [Shiny Core](express-vs-core.qmd), the page layout is explicit, meaning that the UI starts with a page layout component (e.g. `ui.page_fluid()`, `ui.page_sidebar()`, etc). ::: {.column-body-outset-right .panel-tabset .panel-pills .border-0 .justify-content-center} diff --git a/docs/user-interfaces.qmd b/docs/user-interfaces.qmd index 66c5bff3..ab2e2aa2 100644 --- a/docs/user-interfaces.qmd +++ b/docs/user-interfaces.qmd @@ -233,7 +233,7 @@ with ui.popover(title="Popover title"): ``` -### Altogether now +### All together now Let's put it all together to create a dashboard for exploring restaurant tipping data. @@ -277,27 +277,27 @@ with ui.layout_columns(fill=False): with ui.value_box(showcase=ICONS["user"]): "Total tippers" - @render.ui + @render.express def total_tippers(): - return tips_data().shape[0] + tips_data().shape[0] with ui.value_box(showcase=ICONS["wallet"]): "Average tip" - @render.ui + @render.express def average_tip(): d = tips_data() - req(d.shape[0] > 0) - perc = d.tip / d.total_bill - return f"{perc.mean():.1%}" + if d.shape[0] > 0: + perc = d.tip / d.total_bill + f"{perc.mean():.1%}" with ui.value_box(showcase=ICONS["currency-dollar"]): "Average bill" - @render.ui + @render.express def average_bill(): d = tips_data() - req(d.shape[0] > 0) - bill = d.total_bill.mean() - return f"${bill:.2f}" + if d.shape[0] > 0: + bill = d.total_bill.mean() + f"${bill:.2f}" with ui.layout_columns(col_widths=[6, 6, 12]): diff --git a/include-in-header.html b/include-in-header.html index cb16b4dd..12a6b77d 100644 --- a/include-in-header.html +++ b/include-in-header.html @@ -75,7 +75,7 @@ function addWhatsShinyExpressTooltip() { const tooltipContents = `

Shiny Express is a new, streamlined way to write a Shiny app.

Shiny Core refers to the original, functional Shiny syntax, which is still a great way to write Shiny apps.

-

Read more

` +

Read more

` const tooltipDisplay = `What's the difference?` @@ -131,6 +131,6 @@ learn Shiny.

- What about Shiny Core? + What about Shiny Core?