Skip to content

Commit

Permalink
Improve guide docs and include layouts colab (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwillchen authored Aug 14, 2024
1 parent b27b6aa commit c518619
Show file tree
Hide file tree
Showing 13 changed files with 1,301 additions and 46 deletions.
11 changes: 11 additions & 0 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ Although Mesop is pre-v1, we take backwards-compatibilty seriously and avoid bac

Occasionally, we will do minor clean-up for our APIs, but we will provide warnings/deprecation notices and provide at least 1 release to migrate to the newer APIs.

### Which modules should I import from Mesop?

Only import from these two modules:

```py
import mesop as me
import mesop.labs as mel
```

All other modules are considered internal implementation details and may change without notice in future releases.

### Is Mesop an official Google product?

No, Mesop is not an official Google product and Mesop is a 20% project maintained by a small core team of Google engineers with contributions from the broader community.
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ If you make changes to the code, the Mesop app should be automatically hot reloa

Learn more about the core concepts of Mesop as you learn how to build your own Mesop app:

<a href="../core_concepts" class="next-step">
<a href="../core-concepts" class="next-step">
Core Concepts
</a>
49 changes: 46 additions & 3 deletions docs/guides/debugging.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,53 @@
# Debugging

VS Code is recomended for debugging your Mesop app, but you should be able to debug Mesop in other IDEs.
This guide will show you several ways of debugging your Mesop app:

## Debugging in VS Code
- [Debugging with server logs](#debugging-with-server-logs)
- [Debugging with Chrome DevTools](#debugging-with-chrome-devtools)
- [Debugging with VS Code](#debugging-with-vs-code)

**Pre-requisite:** ensure VS Code is downloaded.
You can use the first two methods to debug your Mesop app both locally and in production, and the last one to debug your Mesop app locally.

## Debugging with server logs

If your Mesop app is not working properly, we recommend checking the server logs first.

If you're running Mesop locally, you can check the terminal. If you're running Mesop in production, you will need to use your cloud provider's console to check the logs.

## Debugging with Chrome DevTools

[Chrome DevTools](https://developer.chrome.com/docs/devtools) is a powerful set of web developer tools built directly into the Google Chrome browser. It can be incredibly useful for debugging Mesop applications, especially when it comes to inspecting the client-server interactions.

Here's how you can use Chrome DevTools to debug your Mesop app:

1. Open your Mesop app in Google Chrome.

1. Right-click anywhere on the page and select "Inspect" or use the keyboard shortcut to open Chrome DevTools:
- Windows/Linux: Ctrl + Shift + I
- macOS: Cmd + Option + I

1. To debug general errors:
- Go to the Console tab.
- Look for any console error messages.
- You can also modify the [log levels](https://developer.chrome.com/docs/devtools/console/reference#level) to display Mesop debug logs by clicking on "Default levels" and selecting "Verbose".

1. To debug network issues:
- Go to the [Network tab](https://developer.chrome.com/docs/devtools/network/overview).
- Reload your page to see all network requests.
- Look for any failed requests (they'll be in red).
- Click on a request to see detailed information about headers, response, etc.

1. For JavaScript errors:
- Check the Console tab for any error messages.
- You can set breakpoints in your JavaScript code using the Sources tab.

Remember, while Mesop abstracts away much of the frontend complexity, using these tools can still be valuable for debugging and optimizing your app's performance.

## Debugging with VS Code

VS Code is recommended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs.

**Pre-requisite:** Ensure VS Code is downloaded.

1. Install the [Python Debugger VS Code extension](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy).

Expand Down
179 changes: 179 additions & 0 deletions docs/guides/event-handlers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Event Handlers

Event handlers are a core part of Mesop and enables you to handle user interactions by writing Python functions which are called by the Mesop framework when a user event is received.

## How it works

Let's take a look at a simple example of an event handler:

```py title="Simple event handler"
def counter():
me.button("Increment", on_click=on_click)

def on_click(event: me.ClickEvent):
state = me.state(State)
state.count += 1

@me.stateclass
class State:
count: int = 0
```

Although this example looks simple, there's a lot going on under the hood.

When the counter function is called, it creates an instance of the button component and binds the `on_click` event handler to it. Because components (and the entire Mesop UI) is serialized and sent to the client, we need a way of serializing the event handler so that when the button is clicked, the correct event handler is called on the server.

We don't actually need to serialize the entire event handler, rather we just need to compute a unique id for the event handler function.

Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page may be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault-tolerant and enables simple scaling.

## Types of event handlers

### Regular functions

These are the simplest and most common type of event handlers used. It's essentially a regular Python function which is called by the Mesop framework when a user event is received.

```py title="Regular function"
def on_click(event: me.ClickEvent):
state = me.state(State)
state.count += 1
```

### Generator functions

Python Generator functions are a powerful tool, which allow you to `yield` multiple times in a single event handler. This allows you to render intermediate UI states.

```py title="Generator function"
def on_click(event: me.ClickEvent):
state = me.state(State)
state.count += 1
yield
time.sleep(1)
state.count += 1
yield
```

You can learn more about real-world use cases of the generator functions in the [Interactivity guide](./interactivity.md).

???+ info "Always yield at the end of a generator function"
If you use a `yield` statement in your event handler, then the event handler will be a generator function. You must have a `yield` statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

### Async generator functions

Python async generator functions allow you to do concurrent work using Python's `async` and `await` language features. If you are using async Python libraries, you can use these types of event handlers.

```py title="Async generator function"
async def on_click(event: me.ClickEvent):
state = me.state(State)
state.count += 1
yield
await asyncio.sleep(1)
state.count += 1
yield
```

For a more complete example, please refer to the [Async section of the Interactivity guide](./interactivity.md#async).

???+ info "Always yield at the end of an async generator function"
Similar to a regular generator function, an async generator function must have a `yield` statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

## Patterns

### Reusing event handler logic

You can share event handler logic by extracting the common logic into a separate function. For example, you will often want to use the same logic for the `on_enter` event handler for an input component and the `on_click` event handler for a "send" button component.

```py title="Reusing event handler logic"
def on_enter(event: me.InputEnterEvent):
state = me.state(State)
state.value = event.value
call_api()

def on_click(event: me.ClickEvent):
# Assumes that state.value has been set by an on_blur event handler
call_api()

def call_api():
# Put your common event handler logic here
pass
```

If you want to reuse event handler logic between generator functions, you can use the [`yield from`](https://docs.python.org/3/whatsnew/3.3.html#pep-380) syntax. For example, let's say `call_api` in the above example is a generator function. You can use `yield from` to reuse the event handler logic:

```py title="Reusing event handler logic for generator functions"
def on_enter(event: me.InputEnterEvent):
state = me.state(State)
state.value = event.value
yield from call_api()

def on_click(event: me.ClickEvent):
yield from call_api()

def call_api():
# Do initial work
yield
# Do more work
yield
```
### Boilerplate-free event handlers

If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern which utilizes the `key` attribute that's available in most events and uses Python's built-in `setattr` function to dynamically update the state:

```py title="Boilerplate-free event handlers"
def app():
me.input(label="Name", key="name", on_blur=update_state)
me.input(label="Address", key="address", on_blur=update_state)

@me.stateclass
class State:
name: str
address: str

def update_state(event: me.InputBlurEvent):
state = me.state(State)
setattr(state, event.key, event.value)
```

The downside of this approach is that you lose type safety. Generally, defining a separate event handler, although more verbose, is easier to maintain.

## Troubleshooting

### Avoid using closure variables in event handler

One subtle mistake when building a reusable component is having the event handler use a closure variable, as shown in the following example:

```py title="Bad example of using closure variable"
@me.component
def link_component(url: str):
def on_click(event: me.ClickEvent):
me.navigate(url)
return me.button(url, on_click=on_click)

def app():
link_component("/1")
link_component("/2")
```

The problem with this above example is that Mesop only stores the last event handler. This is because each event handler has the same id which means that Mesop cannot differentiate between the two instances of the same event handler.

This means that both instances of the link_component will refer to the last `on_click` instance which references the same `url` closure variable set to `"/2"`. This almost always produces the wrong behavior.

Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example:

```py title="Good example of using key"
@me.component
def link_component(url: str):
def on_click(event: me.ClickEvent):
me.navigate(event.key)
return me.button(url, key=url, on_click=on_click)
```

For more info on using component keys, please refer to the [Component Key docs](../components/index.md#component-key).

## Next steps

Explore advanced interactivity patterns like streaming and async:

<a href="../interactivity" class="next-step">
Interactivity
</a>
70 changes: 45 additions & 25 deletions docs/guides/interactivity.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Interactivity

This guide continues from the Counter app example in [Core Concepts](../getting-started/core-concepts.md#counter-app) and explains advanced interactivity patterns for dealing with common use cases such as calling a slow blocking API call or a streaming API call.
This guide continues from the [event handlers guide](./event-handlers.md) 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

Expand Down Expand Up @@ -44,7 +44,7 @@ If you notice a race condition with user input (e.g. [input](../components/input

See the following example using this **anti-pattern** :warning::

```py title="Bad example"
```py title="Bad example: setting the value and using on_input"
@me.stateclass
class State:
input_value: str
Expand All @@ -63,50 +63,70 @@ The problem is that the input value now has a race condition because it's being
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.

The way to fix this is by *not* setting the input value from the server.
There's several ways to fix this which are shown below.

The above example **corrected** would look like this :white_check_mark::
#### Option 1: Use `on_blur` instead of `on_input`

```py title="Good example" hl_lines="7"
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.

```py title="Good example: setting the value and using on_input"
@me.stateclass
class State:
input_value: str

def app():
state = me.state(State)
me.input(on_input=on_input)
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
```

### Avoid using closure variables in event handler
#### Option 2: Do not set the input value from the server

One subtle mistake when building a reusable component is to have the event handler use a closure variable like the following example:
If you don't need to set the input value from the server, then you can remove the `value` attribute from the input component.

```py title="Bad example of using closure variable"
@me.component
def link_component(url: str):
def on_click(event: me.ClickEvent):
me.navigate(url)
return me.button(url, on_click=on_click)
```py title="Good example: not setting the value" hl_lines="7"
@me.stateclass
class State:
input_value: str

def app():
link_component("/1")
link_component("/2")
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
```

The problem with this above example is that Mesop only stores the last event handler. This means that both instances of the link_component will refer to the last `on_click` instance which references the same `url` closure variable set to `"/2"`. This almost always produces the wrong behavior.
#### 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.

Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example:
```py title="Good example: using two separate variables for initial and current input value" hl_lines="9"
@me.stateclass
class State:
initial_input_value: str = "initial_value"
current_input_value: str

```py title="Good example of using key"
@me.component
def link_component(url: str):
def on_click(event: me.ClickEvent):
me.navigate(event.key)
return me.button(url, key=url, on_click=on_click)
@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
```

For more info on using component keys, please refer to the [Component Key docs](../components/index.md#component-key).
## Next steps

Learn about layouts to build a customized UI.

<a href="../layouts" class="next-step">
Layouts
</a>
Loading

0 comments on commit c518619

Please sign in to comment.