Skip to content

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 ⚠:

Bad example: setting the value and using on_input
@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:

  1. The server is setting the input value based on state.
  2. 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.

Good example: setting the value and using on_input
@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.

Good example: not setting the value
@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.

Good example: using two separate variables for 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.

Layouts