diff --git a/doc/_static/bar_chart_race.mp4 b/doc/_static/bar_chart_race.mp4
new file mode 100644
index 0000000000..9d905b367f
Binary files /dev/null and b/doc/_static/bar_chart_race.mp4 differ
diff --git a/doc/tutorials/basic/build_animation.md b/doc/tutorials/basic/build_animation.md
index aa48fa2df9..526150fb3a 100644
--- a/doc/tutorials/basic/build_animation.md
+++ b/doc/tutorials/basic/build_animation.md
@@ -2,11 +2,22 @@
In this tutorial, we will create a *bar chart race* using the [Altair](https://altair-viz.github.io) plotting library and the [`Player`](../../reference/widgets/Player.ipynb) widget.
+*Todo: The below is for testing on ios. Clean up*
+
+https://assets.holoviz.org/panel/tutorials/bar_chart_race.mp4 NOT PLAYING ON IPHONE
+
+../../_static/bar_chart_race_2.mp4 IS PLAYING ON IPHONE
+
+
+
:::{dropdown} Dependencies
```bash
diff --git a/doc/tutorials/basic/build_todo.md b/doc/tutorials/basic/build_todo.md
index 0e06088806..23667f1d75 100644
--- a/doc/tutorials/basic/build_todo.md
+++ b/doc/tutorials/basic/build_todo.md
@@ -11,7 +11,7 @@ In this section, we will work on building a *Todo App* together so that our wind
When we ask everyone to *run the code* in the sections below, you may either execute the code directly in the Panel docs via the green *run* button, in a cell in a notebook, or in a file `app.py` that is served with `panel serve app.py --autoreload`.
:::
-
+
:::{dropdown} Requirements
diff --git a/doc/tutorials/basic/index.md b/doc/tutorials/basic/index.md
index a02231b786..fc8e0c1b7d 100644
--- a/doc/tutorials/basic/index.md
+++ b/doc/tutorials/basic/index.md
@@ -46,15 +46,62 @@ Start your journey with these foundational tutorials:
## Master Panel Basics
-Once you're comfortable, it's time to dive deeper:
+Once you're comfortable, it's time to dive deeper. Through a series of lessons we will learn about displaying content, arranging it on the page, handling user inputs and then how to improve the UI and UX of our applications.
-| Part | Section A | Section B| Section C |
-|--------------------------|---------------------------|-------------------|------------------------------------------------------|
-| **1. Display Content** | [`pn.panel`](pn_panel.md) | [Panes](panes.md) | [Performance Indicators](indicators_performance.md) |
-| **2. Organize Content** | [Layouts](layouts.md) | [Control the Size](size.md) | [Align Content](align.md) |
-| **3. Handle User Input** | [Widgets](widgets.md) | [React to User Input](pn_bind.md) | [Handle State](state.md) |
-| **4. Improve the look** | [Templates](templates.md)| [Designs](design.md) | [Styles](style.md) |
-| **5. Improve the Feel** | [Caching](caching.md) | [Activity Indicators](indicators_activity.md) | [Progressive Updates](progressive_layouts.md) |
+### 1. Display Content
+
+[`pn.panel`](pn_panel.md)
+: Learn to display Python objects easily with `pn.panel`
+
+[Panes](panes.md)
+: Learn to display content by creating Panes.
+
+[Indicators](indicators_performance.md)
+: Visualize key metrics with simple indicators
+
+### 2. Organize Content
+
+[Layouts](layouts.md)
+: Arrange output on the page using layouts.
+
+[Control the Size](size.md)
+: Learn to control the sizing of your components.
+
+[Align Content](align.md)
+: Discover how to align content on the page.
+
+### 3. Handle User Input
+
+[Widgets](widgets.md)
+: Learn about handling user input with widgets.
+
+[React to User Input](pn_bind.md)
+: Learn about reacting to user input by binding it to interactive components.
+
+[Reactive Expressions](pn_rx.md)
+: Learn about handling state and writing entire reactive expressions.
+
+### 4. Improve the Look
+
+[Templates](templates.md)
+: Learn to structure your app with pre-built templates.
+
+[Designs](design.md)
+: Style your apps using pre-built *designs*
+
+[Styles](style.md)
+: Further customize the look and feel by adding CSS styling.
+
+### 5. Improve the Feel
+
+[Caching](caching.md)
+: Leverage caching to enhance the speed and efficiency of your app.
+
+[Activity Indicators](indicators_activity.md)
+: Indicate progress and add notifications to improve the user experience.
+
+[Progressive Updates](progressive_layouts.md)
+: Efficiently and effortlessly update the content in your app with progressive updates.
## Share Your Creations
@@ -77,6 +124,17 @@ Now that you've got the basics down, it's time to put your skills to the test:
Let's start building some amazing wind turbine apps! 🌬️🌀
+## Community Tutorials
+
+Want more? Check out some amazing tutorials by the community.
+
+- [3 Ways to Build a Panel Visualization Dashboard - Sophia Yang](https://towardsdatascience.com/3-ways-to-build-a-panel-visualization-dashboard-6e14148f529d) (Blog Post) | [PyTexas 2023](https://www.youtube.com/watch?v=8du4NNoOtII) (Video)
+- [HoloViz: Visualization and Interactive Dashboards in Python - Jean-Luc Stevens](https://www.youtube.com/watch?v=61uHwBlxRug) (Video)
+- [How to create Data Analytics Visualisation Dashboard using Python with Panel/Hvplot in just 10 mins - Atish Jain](https://www.youtube.com/watch?v=__QUQg96SFs) (Video)
+- [Step-by-Step Guide to Create Multi-Page Dashboard using Panel - CoderzColumn](https://www.youtube.com/watch?v=G3M0lQcWpqE) (Video)
+- [Transform a Python script into an interactive, web app and make it performant - Andrew Huang](https://blog.stackademic.com/transform-a-python-script-into-an-interactive-web-app-and-make-it-performant-73fa3b304cdf) (Blog Post)
+- [Using Panel to Build Data Dashboards in Python - Will Norris](https://towardsdatascience.com/using-panel-to-build-data-dashboards-in-python-e87a04c9034d) (Blog Post)
+
```{toctree}
:titlesonly:
:hidden:
@@ -93,7 +151,7 @@ size
align
widgets
pn_bind
-state
+pn_rx
caching
indicators_activity
progressive_layouts
diff --git a/doc/tutorials/basic/indicators_performance.md b/doc/tutorials/basic/indicators_performance.md
index 55c6be4294..66e18b2fe8 100644
--- a/doc/tutorials/basic/indicators_performance.md
+++ b/doc/tutorials/basic/indicators_performance.md
@@ -4,11 +4,14 @@ Welcome to our tutorial on Panel's [indicators](https://panel.holoviz.org/refer
We will delve into the use of Indicators with an example that uses them to visualizes the key metrics of wind turbines in an engaging and insightful manner. The result will be this:
-:::{grid-item-card} Windturbines Explorer
-
-:target: https://|gallery-endpoint|.pyviz.demo.anaconda.com/windturbines
+:::{card} Windturbines Explorer
+:link: https://|gallery-endpoint|.holoviz.dsp.anaconda.com/windturbines
:width: 100%
-Visually explore a dataset of US Windturbines. +++ See source :::
+
+Visually explore a dataset of US Windturbines.
++++
+[See source](../../gallery/windturbines)
+:::
## Explore Indicators
diff --git a/doc/tutorials/basic/panes.md b/doc/tutorials/basic/panes.md
index 29f23d5535..a7b8b30f0a 100644
--- a/doc/tutorials/basic/panes.md
+++ b/doc/tutorials/basic/panes.md
@@ -10,6 +10,10 @@ In this tutorial, we will learn to display objects with *Panes*:
A *Pane* is a component that can display an object. It takes an `object` as an argument.
:::
+:::{note}
+You might notice a lot of repetition from the previous section regarding `pn.panel`. Don't worry, it's intentional! We're doing this to empower you with the ability to compare and contrast. While `pn.panel` is incredibly user-friendly and versatile, specific Panes allow you to display output with precision and efficiency. This enables you to construct more intricate and high-performing applications.
+:::
+
:::{note}
When we ask you to *run the code* in the sections below, you may either execute the code directly in the Panel docs via the green *run* button, in a cell in a notebook, or in a file `app.py` that is served with `panel serve app.py --autoreload`.
:::
diff --git a/doc/tutorials/basic/pn_bind.md b/doc/tutorials/basic/pn_bind.md
index 78591f73b1..8f9e1af0da 100644
--- a/doc/tutorials/basic/pn_bind.md
+++ b/doc/tutorials/basic/pn_bind.md
@@ -1,9 +1,337 @@
# React to User Input
-COMING UP
+Welcome to the interactive world of Panel! In this section, you'll learn how to make your Panel applications come alive by reacting to user input. We'll explore how to bind widgets to a function and add side effects using the `watch` parameter in Panel.
+
+## Embrace `pn.bind`
+
+The `pn.bind` method is your gateway to interactive Panel applications. It enables you to build interactive components that respond to user inputs simply by binding widgets to functions. Let's dive into an example:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+def calculate_power(wind_speed, efficiency):
+ power_generation = wind_speed * efficiency
+ return (
+ f"Wind Speed: {wind_speed} m/s, "
+ f"Efficiency: {efficiency}, "
+ f"Power Generation: {power_generation:.1f} kW"
+ )
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+
+efficiency = 0.3
+
+power = pn.bind(
+ calculate_power, wind_speed=wind_speed, efficiency=efficiency
+)
+
+pn.Column(wind_speed, power).servable()
+```
+
+As you interact with the slider, notice how the displayed power generation dynamically updates, reflecting changes in wind speed.
+
+You can of course bind multiple widgets. Let's make the `efficiency` a widget:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+def calculate_power(wind_speed, efficiency):
+ power_generation = wind_speed * efficiency
+ return (
+ f"Wind Speed: {wind_speed} m/s, "
+ f"Efficiency: {efficiency}, "
+ f"Power Generation: {power_generation:.1f} kW"
+ )
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")
+
+power = pn.bind(
+ calculate_power, wind_speed=wind_speed, efficiency=efficiency
+)
+
+pn.Column(wind_speed, efficiency, power).servable()
+```
+
+## Using References
+
+Bound functions can be displayed directly as we have done above or they can be used as references when passed to a Panel component. This approach is usually more efficient since we only have to update the specific parameter:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+def calculate_power(wind_speed, efficiency):
+ power_generation = wind_speed * efficiency
+ return (
+ f"Wind Speed: {wind_speed} m/s, "
+ f"Efficiency: {efficiency}, "
+ f"Power Generation: {power_generation:.1f} kW"
+ )
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")
+
+power = pn.bind(
+ calculate_power, wind_speed=wind_speed, efficiency=efficiency
+)
+
+power_md = pn.pane.Markdown(power)
+
+pn.Column(wind_speed, efficiency, power_md).servable()
+```
+
+Note how we pass the bound function as an argument to the `Markdown` pane. This way the Markdown pane only has to send the updated text.
+
+## Crafting Interactive Forms
+
+Forms are powerful tools for collecting user inputs. With Panel, you can easily create forms and process them after they are submitted:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+def calculate_power(wind_speed, efficiency):
+ power_generation = wind_speed * efficiency
+ return (
+ f"Wind Speed: {wind_speed} m/s, "
+ f"Efficiency: {efficiency}, "
+ f"Power Generation: {power_generation:.1f} kW"
+ )
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")
+
+power = pn.bind(
+ calculate_power, wind_speed=wind_speed, efficiency=efficiency
+)
+
+submit = pn.widgets.Button(name="Submit", button_type="primary")
+
+def result(clicked):
+ if clicked:
+ return power()
+ return "Click Submit"
+
+result = pn.pane.Markdown(pn.bind(result, submit))
+
+pn.Column(
+ wind_speed, efficiency, submit, result
+).servable()
+```
+
+Notice how the text is updated only when the Submit Button is clicked.
+
+## Harnessing Throttling for Performance
+
+To prevent excessive updates and ensure smoother performance, you can apply throttling by binding the `value_throttled` parameter. This limits the rate at which certain actions or events occur, maintaining a balanced user experience:
+
+```{pyodide}
+import panel as pn
+
+from time import sleep
+
+pn.extension()
+
+def calculate_power(wind_speed, efficiency):
+ power_generation = wind_speed * efficiency
+ return (
+ f"Wind Speed: {wind_speed} m/s, "
+ f"Efficiency: {efficiency}, "
+ f"Power Generation: {power_generation:.1f} kW"
+ )
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+
+efficiency = 0.3
+
+calculate_power_bnd = pn.bind(
+ calculate_power, wind_speed=wind_speed.param.value_throttled, efficiency=efficiency
+)
+
+power_md = pn.pane.Markdown(power)
+
+pn.Column(wind_speed, power_md).servable()
+```
+
+Try dragging the slider. Notice that the `calculate_power` function is only run when you release the mouse.
+
+### Binding to bound functions
+
+Bound functions can themselves be bound to other functions. This can help you break down you reactivity into smaller, reusable steps.
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+def calculate_power(wind_speed, efficiency):
+ return wind_speed * efficiency
+
+def format_power_gen(wind_speed, efficiency, power):
+ return (
+ f"Wind Speed: {wind_speed} m/s, "
+ f"Efficiency: {efficiency}, "
+ f"Power Generation: {power:.1f} kW"
+ )
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+
+efficiency = 0.3
+
+power = pn.bind(calculate_power, wind_speed, efficiency)
+
+power_text = pn.bind(format_power_gen, wind_speed, efficiency, power)
+
+pn.Column(wind_speed, power, power_text).servable()
+```
+
+:::{warning}
+Binding to bound functions can help you to quickly explore your data, but it can be inefficient as the results are calculated from scratch for each call.
+:::
+
+Try changing the `power_generation` function to:
+
+```python
+def power_generation(wind_speed, efficiency):
+ print(wind_speed, efficiency)
+ return wind_speed * efficiency
+```
+
+Try dragging the `wind_speed` slider. Notice that the `power_generation` function is called twice every time you change the `wind_speed` `value`.
+
+To solve this problem you should add *caching* (`pn.cache`) or use *reactive expressions* (`pn.rx`). You will learn about *reactive expressions* in the next section.
+
+## Triggering Side Effects with `watch`
+
+When you need to trigger additional tasks in response to user actions, setting `watch` comes in handy:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+submit = pn.widgets.Button(name="Start the wind turbine")
+
+def start_stop_wind_turbine(clicked):
+ if submit.clicks % 2:
+ submit.name = "Start the wind turbine"
+ else:
+ submit.name = "Stop the wind turbine"
+
+pn.bind(start_stop_wind_turbine, submit, watch=True)
+
+pn.Column(submit).servable()
+```
+
+```{warning}
+In the example provided, our side effect directly modifies the UI by altering the name of the Button. However, this approach indicates poor architectural design.
+
+It's advisable to avoid directly updating the UI through side effects. Instead, focus on updating the application's *state*, allowing the UI to respond automatically to any changes in the state. The concept of state will be explored further in the subsequent section.
+```
+
+If your task is long running you might want to disable the `Button` and add a loading indicator while the task is running.
+
+```{pyodide}
+import time
+import panel as pn
+
+pn.extension()
+
+submit = pn.widgets.Button(name="Start the wind turbine")
+
+def start_stop_wind_turbine(clicked):
+ with submit.param.update(loading=True, disabled=True):
+ time.sleep(2)
+ if bool(submit.clicks%2):
+ submit.name = "Start the wind turbine"
+ else:
+ submit.name = "Stop the wind turbine"
+
+pn.bind(start_stop_wind_turbine, submit, watch=True)
+
+pn.Column(submit).servable()
+```
+
+### Keep the UI responsive with threads or processes
+
+To keep your UI and server responsive while the long running, blocking task is running you might want to run it asynchronously in a separate thread:
+
+```python
+from time import sleep
+
+import asyncio
+
+import panel as pn
+
+pn.extension()
+
+submit = pn.widgets.Button(name="Start the wind turbine")
+
+async def start_stop_wind_turbine(clicked):
+ with submit.param.update(loading=True, disabled=True):
+ result = await asyncio.to_thread(sleep, 5)
+
+ if submit.clicks % 2:
+ submit.name = "Start the wind turbine"
+ else:
+ submit.name = "Stop the wind turbine"
+
+pn.bind(start_stop_wind_turbine, submit, watch=True)
+
+pn.Column(submit).servable()
+```
+
+:::{note}
+In the example we use a `asyncio.to_thread` this should work great if your blocking task releases the GIL while running. Tasks that request data from the web or read data from files typically do this. Some computational methods from Numpy, Pandas etc. also release the GIL.
+
+If your long running task does not release the GIL you may have to use a `ProcessPoolExecutor` instead. This introduces some overhead though.
+:::
+
+## Recap
+
+You've now unlocked the power of interactivity in your Panel applications:
+
+- `pn.bind(some_function, widget_1, widget_2)`: for seamless updates based on widget values.
+- `pn.bind(some_task, some_widget, watch=True)`: for triggering tasks in response to user actions.
+- Throttling ensures smoother performance by limiting update frequency.
+- Utilizing async and threading keeps your UI responsive during long-running tasks.
+
+Now, let your imagination run wild and craft dynamic, engaging Panel applications!
## Resources
### How-to
- [Add interactivity to a function](../../how_to/interactivity/bind_function.md)
+- [Add Interactivity with `pn.bind` | Migrate from Streamlit](../../how_to/streamlit_migration/interactivity.md)
+- [Enable Throttling](../../how_to/performance/throttling.md)
+- [Run synchronous functions asynchronously](../../how_to/concurrency/sync_to_async.md)
+- [Setup Manual Threading](../../how_to/concurrency/manual_threading.md)
+- [Use Asynchronous Callbacks](../../how_to/callbacks/async.md)
+
+### Explanation
+
+- [Reactivity in Panel](../../explanation/api/reactivity.md)
+
+### External
+
+- [Param: Parameters and `param.bind`](https://param.holoviz.org/user_guide/Reactive_Expressions.html#parameters-and-param-bind)
diff --git a/doc/tutorials/basic/pn_rx.md b/doc/tutorials/basic/pn_rx.md
new file mode 100644
index 0000000000..3882ac50ac
--- /dev/null
+++ b/doc/tutorials/basic/pn_rx.md
@@ -0,0 +1,258 @@
+# Reactive Expressions
+
+In this section you will learn about `pn.rx`. `pn.rx` extends the concepts from `pn.bind` that your learned in the previous section.
+
+:::{note}
+You might feel some repetition from the previous section on `pn.bind`. We do this on purpose to enable you to compare and contrast. `pn.rx` is the an extension of `pn.bind` that provides more power and flexibility. `pn.bind` has been the core API in Panel for a long time, so you will meet it across our documentation and community sites, and thus its very important to learn.
+
+`pn.rx` will enable you to build more complicated applications using a more flexible and maintainable architecture.
+:::
+
+## Embrace `pn.rx`
+
+`pn.rx` allows you to treat any object as a reactive expression. This means we can do things like multiplying a widget (representing the wind speed) with a float value (representing the efficiency) and then format the result all without writing callbacks:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+
+efficiency = 0.3
+
+power = wind_speed.rx() * efficiency
+
+power_text = pn.rx(
+ "Wind Speed: {wind_speed} m/s, "
+ "Efficiency: {efficiency}, "
+ "Power Generation: {power:.1f} kW"
+).format(wind_speed=wind_speed, efficiency=efficiency, power=power)
+
+pn.Column(power_text).servable()
+```
+
+You will notice how adding `power_text` to the `Column` displays both the widget and the bound function.
+
+To separate the widget and bound function we can use the `power_text` as a reference when we construct a `Markdown` pane:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+
+efficiency = 0.3
+
+power = wind_speed.rx() * efficiency
+
+power_text = pn.rx(
+ "Wind Speed: {wind_speed} m/s, "
+ "Efficiency: {efficiency}, "
+ "Power Generation: {power:.1f} kW"
+).format(wind_speed=wind_speed, efficiency=efficiency, power=power)
+
+power_md = pn.pane.Markdown(power_text)
+
+pn.Column(wind_speed, power_md).servable()
+```
+
+You can of course write expressions with multiple widgets. Lets make the `efficiency` a widget:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+efficiency = pn.widgets.FloatInput(value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))")
+
+power = wind_speed.rx() * efficiency.rx()
+
+power_text = pn.rx(
+ "Wind Speed: {wind_speed} m/s, "
+ "Efficiency: {efficiency}, "
+ "Power Generation: {power:.1f} kW"
+).format(wind_speed=wind_speed, efficiency=efficiency, power=power)
+
+power_md = pn.pane.Markdown(power_text)
+
+pn.Column(wind_speed, efficiency, power_md).servable()
+```
+
+## Crafting Interactive Forms
+
+Forms are powerful tools for collecting user inputs. With `.rx.when` you can easily defer some calculation (i.e. the form submission) until some event (such as a button click) is triggered:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+efficiency = pn.widgets.FloatInput(
+ value=0.3, start=0.0, end=1.0, name="Efficiency (kW/(m/s))"
+)
+submit = pn.widgets.Button(name="Submit", button_type="primary")
+
+power = wind_speed.rx() * efficiency.rx()
+
+power_text = pn.rx(
+ "Wind Speed: {wind_speed} m/s, "
+ "Efficiency: {efficiency}, "
+ "Power Generation: {power:.1f} kW"
+).format(
+ wind_speed=wind_speed,
+ efficiency=efficiency,
+ power=power
+).rx.when(submit)
+
+pn.Column(
+ wind_speed, efficiency, submit, pn.pane.Markdown(power_text)
+).servable()
+```
+
+Try changing some of the inputs and clicking the submit Button. Try again. Notice how the text is only updated when we click the submit Button - we used `.rx.when` to achieve this effect.
+
+## Harnessing Throttling for Performance
+
+To prevent excessive updates and ensure smoother performance, you can apply throttling (`.value_throttled`). This limits the rate at which certain actions or events occur, maintaining a balanced user experience:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+wind_speed = pn.widgets.FloatSlider(
+ value=5, start=0, end=20, step=1, name="Wind Speed (m/s)"
+)
+efficiency = 0.3
+
+power = wind_speed.param.value_throttled.rx() * efficiency
+
+power_text = pn.rx(
+ "Wind Speed: {wind_speed} m/s, "
+ "Efficiency: {efficiency}, "
+ "Power Generation: {power:.1f} kW"
+).format(
+ wind_speed=wind_speed.param.value_throttled,
+ efficiency=0.3,
+ power=power
+)
+
+pn.Column(
+ wind_speed, pn.pane.Markdown(power_text)
+).servable()
+```
+
+Try dragging the slider. Notice that the text is only updated when you release the mouse.
+
+## Triggering Side Effects with `.watch`
+
+When you need to trigger additional tasks in response to user actions, using `.watch` comes in handy:
+
+```{pyodide}
+import panel as pn
+
+pn.extension()
+
+# Declare state of application
+is_stopped = pn.rx(True)
+
+rx_name = is_stopped.rx.where("Start the wind turbine", "Stop the wind turbine")
+
+submit = pn.widgets.Button(name=rx_name)
+
+def toggle_wind_turbine(clicked):
+ is_stopped.rx.value = not is_stopped.rx.value
+
+submit.rx.watch(toggle_wind_turbine)
+
+pn.Column(submit).servable()
+```
+
+Here we store the state of the windturbine in a separate `rx` variable, whenever the submit button is clicked we toggle the state.
+
+:::{tip}
+When building apps using functions we recommend this approach to storing state. Applications are easier to reason about when the application displays according to the state instead of being changed directly.
+:::
+
+### Keep the UI responsive with threads or processes
+
+To keep your UI and server responsive while the long running, blocking task is running you might want to run it asyncrounously in a separate thread:
+
+```python
+import asyncio
+import time
+
+import panel as pn
+
+pn.extension()
+
+is_stopped = pn.rx(True)
+
+submit = pn.widgets.Button(
+ name=is_stopped.rx.where("Start the wind turbine", "Stop the wind turbine"),
+)
+
+async def start_stop_wind_turbine(clicked):
+ with submit.param.update(loading=True, disabled=True):
+ await asyncio.to_thread(time.sleep, 1)
+ is_stopped.rx.value = not is_stopped.rx.value
+
+submit.rx.watch(start_stop_wind_turbine)
+
+pn.Column(submit).servable()
+```
+
+:::{note}
+In the example we use a `asyncio.to_thread` this should work great if your blocking task releases the GIL while running. Tasks that request data from the web or read data from files typically do this. Some computational methods from Numpy, Pandas etc. also release the GIL. If your long running task does not release the GIL you may have to use a `ProcessPoolExecutor` instead. This introduces some overhead though.
+:::
+
+## Recommended Reading
+
+We do recommend you study the [`ReactiveExpr` reference guide](../../reference/panes/ReactiveExpr.ipynb) to learn more about displaying reactive expressions in Panel.
+
+## Recap
+
+You've now unlocked the power of interactivity in your Panel applications:
+
+- `some_widget.rx()`: for seamless updates based on widget values.
+- `pn.rx(some_function)(widget_1, widget_2)`: for seamless updates based on widget values.
+- `pn.rx(some_task, some_widget).rx.watch()`: for triggering tasks in response to user actions.
+- Throttling ensures smoother performance by limiting update frequency.
+- Utilizing async and threading keeps your UI responsive during long-running tasks.
+
+Now, let your imagination run wild and craft dynamic, engaging Panel applications!
+
+## Resources
+
+### Reference Guides
+
+[`ReactiveExpr` reference guide](../../reference/panes/ReactiveExpr.ipynb)
+
+### How-to
+
+- [Add interactivity to a function](../../how_to/interactivity/bind_function.md)
+- [Add Interactivity with `pn.bind` | Migrate from Streamlit](../../how_to/streamlit_migration/interactivity.md)
+- [Enable Throttling](../../how_to/performance/throttling.md)
+- [Run synchronous functions asynchronously](../../how_to/concurrency/sync_to_async.md)
+- [Setup Manual Threading](../../how_to/concurrency/manual_threading.md)
+- [Use Asynchronous Callbacks](../../how_to/callbacks/async.md)
+
+### Explanation
+
+- [Reactivity](../../explanation/api/reactivity.md)
+
+### External
+
+- [Param: Reactive Functions and Expressions](https://param.holoviz.org/user_guide/Reactive_Expressions.html)
diff --git a/doc/tutorials/basic/state.md b/doc/tutorials/basic/state.md
deleted file mode 100644
index bfb0f979da..0000000000
--- a/doc/tutorials/basic/state.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Handle State
-
-COMING UP
-
-Build larger and more complex apps by defining and maintaining state via `pn.rx`.