Interactivity¶
This guide continues from the event handlers guide and explains advanced interactivity patterns for dealing with common use cases such as calling a slow blocking API call or a streaming API call.
Intermediate loading state¶
If you are calling a slow blocking API (e.g. several seconds) to provide a better user experience, you may want to introduce a custom loading indicator for a specific event.
Note: Mesop has a built-in loading indicator at the top of the page for all events.
import time
import mesop as me
def slow_blocking_api_call():
time.sleep(2)
return "foo"
@me.stateclass
class State:
data: str
is_loading: bool
def button_click(event: me.ClickEvent):
state = me.state(State)
state.is_loading = True
yield
data = slow_blocking_api_call()
state.data = data
state.is_loading = False
yield
@me.page(path="/loading")
def main():
state = me.state(State)
if state.is_loading:
me.progress_spinner()
me.text(state.data)
me.button("Call API", on_click=button_click)
In this example, our event handler is a Python generator function. Each yield
statement yields control back to the Mesop framework and executes a render loop which results in a UI update.
Before the first yield statement, we set is_loading
to True on state so we can show a spinner while the user is waiting for the slow API call to complete.
Before the second (and final) yield statement, we set is_loading
to False, so we can hide the spinner and then we add the result of the API call to state so we can display that to the user.
Tip: you must have a yield statement as the last line of a generator event handler function. Otherwise, any code after the final yield will not be executed.
Streaming¶
This example builds off the previous Loading example and makes our event handler a generator function so we can incrementally update the UI.
from time import sleep
import mesop as me
def generate_str():
yield "foo"
sleep(1)
yield "bar"
@me.stateclass
class State:
string: str = ""
def button_click(action: me.ClickEvent):
state = me.state(State)
for val in generate_str():
state.string += val
yield
@me.page(path="/streaming")
def main():
state = me.state(State)
me.button("click", on_click=button_click)
me.text(text=f"{state.string}")
Async¶
If you want to do multiple long-running operations concurrently, then we recommend you to use Python's async
and await
.
import asyncio
import mesop as me
@me.page(path="/async_await")
def page():
s = me.state(State)
me.text("val1=" + s.val1)
me.text("val2=" + s.val2)
me.button("async with yield", on_click=click_async_with_yield)
me.button("async without yield", on_click=click_async_no_yield)
@me.stateclass
class State:
val1: str
val2: str
async def fetch_dummy_values():
# Simulate an asynchronous operation
await asyncio.sleep(2)
return "<async_value>"
async def click_async_with_yield(e: me.ClickEvent):
val1_task = asyncio.create_task(fetch_dummy_values())
val2_task = asyncio.create_task(fetch_dummy_values())
me.state(State).val1, me.state(State).val2 = await asyncio.gather(
val1_task, val2_task
)
yield
async def click_async_no_yield(e: me.ClickEvent):
val1_task = asyncio.create_task(fetch_dummy_values())
val2_task = asyncio.create_task(fetch_dummy_values())
me.state(State).val1, me.state(State).val2 = await asyncio.gather(
val1_task, val2_task
)
Troubleshooting¶
User input race condition¶
If you notice a race condition with user input (e.g. input or textarea) where sometimes the last few characters typed by the user is lost, you are probably unnecessarily setting the value of the component.
See the following example using this anti-pattern :
@me.stateclass
class State:
input_value: str
def app():
state = me.state(State)
me.input(value=state.input_value, on_input=on_input)
def on_input(event: me.InputEvent):
state = me.state(State)
state.input_value = event.value
The problem is that the input value now has a race condition because it's being set by two sources:
- The server is setting the input value based on state.
- The client is setting the input value based on what the user is typing.
There's several ways to fix this which are shown below.
Option 1: Use on_blur
instead of on_input
¶
You can use the on_blur
event instead of on_input
to only update the input value when the user loses focus on the input field.
This is also more performant because it sends much fewer network requests.
@me.stateclass
class State:
input_value: str
def app():
state = me.state(State)
me.input(value=state.input_value, on_input=on_input)
def on_input(event: me.InputEvent):
state = me.state(State)
state.input_value = event.value
Option 2: Do not set the input value from the server¶
If you don't need to set the input value from the server, then you can remove the value
attribute from the input component.
@me.stateclass
class State:
input_value: str
def app():
state = me.state(State)
me.input(on_input=on_input)
def on_input(event: me.InputEvent):
state = me.state(State)
state.input_value = event.value
Option 3: Use two separate variables for initial and current input value¶
If you need set the input value from the server and you need to use on_input
, then you can use two separate variables for the initial and current input value.
@me.stateclass
class State:
initial_input_value: str = "initial_value"
current_input_value: str
@me.page()
def app():
state = me.state(State)
me.input(value=state.initial_input_value, on_input=on_input)
def on_input(event: me.InputEvent):
state = me.state(State)
state.current_input_value = event.value
Next steps¶
Learn about layouts to build a customized UI.