Skip to content

Instantly share code, notes, and snippets.

@rmorshea
Last active July 10, 2023 08:22
Show Gist options
  • Save rmorshea/ce96215f31fb1a895c3ab7c1c94c3b26 to your computer and use it in GitHub Desktop.
Save rmorshea/ce96215f31fb1a895c3ab7c1c94c3b26 to your computer and use it in GitHub Desktop.
ReactPy in the Browser

A Vision for ReactPy 2.0 (maybe 3.0?)

Currently, ReactPy runs entirely server-side - all views are rendered, and their state is maintained on the server. This is how most other applications that have given Python the ability to drive interactivity have been built. Examples can be found in:

  • IpyWidgets
  • Solara
  • Streamlit
  • Pynecone

This design has opened up a whole world of possibilities for applications written purely with Python, however it has a significant drawback. In particular, this approach of requiring the server to retain a client's state and render views means that you are fundamentally limited in how many users you can serve. In many cases this has meant that these tools tend to be used to build interal data apps that serve a small number of users. Traditionally built applications avoid this problem by delegating much of this work to the client, only reaching out to a server when absolutely necessary. Even though tools like PyScript and Pyodide now allow Python to run client-side, with that alone, you're still just building the same style of traditional app but with a pure Python stack. You still have to go to the effort of creating and maintaining an API for your server. With Python running fully server-side the friction associated with server APIs is entirely avoided since all your data is directly accessible.

At the moment, ReactPy is faced with the same dilema - it can run fully server-side but with scalability problems or it could run entirely client-side but be faced with the friction of dealing with a server. To move beyond these issues ReactPy needs to take a different approach. Thankfully, as has been the case from the beginning of this project, we can take inspiration from ReactJS...

Server-side components a la ReactJS

The creators of ReactJS observed similar problems around the friction of dealing with server APIs and so introduced React Server Components (RSCs). In their own words "RSCs combine the simple request/response mental model of server-centric Multi-Page Apps with the seamless interactivity of client-centric Single-Page Apps." A high level summary of ReactJS's SSCs can be found in this video and a more detailed discussion in the associated RFC. In short though, RSCs are an async, and stateless version of their client-side counterparts - they are async because they are allowed to fetch data directly in the body of the component and they are stateless because they are not allowed to use hooks.

If implemented in ReactPy, a server component would simply be designated as such using async def:

@component
async def my_server_component():
    ...

As mentioned earlier, a key distinction between client and server components is the fact that RSCs must be stateless. As such they are not allowed to use hooks. Instead though, RSCs are allowed to perform data fetching directly in the body of the component unlike a typical client-side component which would require the use_effect hook:

@component
async def artist_page(artist_id):
    artist = await get_artist(artist_id)
    return html._(
        html.h1(artist.name),
        artist_albums(artist.albums),
    )

When you need interactivity though, server and client components can be used together. For example, you might have a server component that fetches a list of artists and a client component that allows you to filter that list:

@component
async def artist_list():
    artists = await get_artists()
    return html._(
        html.h1("Artists"),
        artist_filter(artist),
    )

@component
def artist_filter(artists):
    filter = use_state("")
    filtered_artists = [artist for artist in artists if filter in artist.name]
    return html._(
        html.input(on_change=lambda e: filter.set(e.target.value)),
        html.ul([html.li({"key": artist.name}, artist.name) for artist in filtered_artists]),
    )

We'll be designing a detailed proposal for server components in this repository for ReactPy Enhancement Proposals or (RPEPs).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment