coderquill's inklings

Building a React-like Component Framework on Top of Streamlit

As a developer who enjoys exploring the intersection of frontend and backend development, I recently embarked on a journey to build a React-like component framework on top of Streamlit. While Streamlit is known for its simplicity in creating data-driven apps, I wanted to push its boundaries by introducing concepts like components, hooks, and a virtual DOM that React developers are familiar with.

In this post, I’ll share my learning experience, the challenges I faced, and how I implemented a basic framework that mimics React’s useState hook and component system using Streamlit’s existing capabilities.

Table of Contents

Motivation

Streamlit’s auto-reload mechanism and top-down execution model make it ideal for quickly building interactive applications. However, I wanted to experiment with a component-based architecture on top of Streamlit to see if I could build reusable UI elements, manage state more predictably, and understand how React-like patterns could coexist with Streamlit’s execution flow.

Goals of the Project

Implementation Overview

Here’s how I structured the framework and tackled each of the goals:

1. Creating a Virtual DOM (VNode)

The core of any React-like framework is its ability to represent the UI as a tree of nodes. I introduced a simple VNode class to represent elements like text, buttons, containers, and more.

# VNode class to represent a virtual DOM node
class VNode:
    def __init__(self, type, key=None, props=None, children=None):
        self.type = type  # Type of element (e.g., "text", "button", "container")
        self.key = key  # Unique key to identify the node
        self.props = props or {}  # Properties (e.g., content, label)
        self.children = children or []  # Child nodes

    def __repr__(self):
        return f"VNode(type={self.type}, key={self.key}, props={self.props})"

2. Building a Component System

To build components like React, I created a base Component class that could manage its own state and handle re-renders. I implemented a custom hook called use_state to mimic React’s useState hook.

# Base component class that manages state and triggers re-renders
class Component:
    def __init__(self):
        self.state = {}

    def set_state(self, new_state):
        """
        Set the component's state and trigger a re-render through Streamlit.
        """
        self.state.update(new_state)
        st.experimental_rerun()  # Force Streamlit to rerun and apply state changes

3. Implementing use_state

The use_state hook is responsible for managing individual pieces of state for each component. In React, useState updates the state and triggers a re-render of the component. I implemented a similar mechanism using Streamlit’s session_state to persist state values.

import streamlit as st

def use_state(key, initial_value=None):
    if key not in st.session_state:
        st.session_state[key] = initial_value
    return st.session_state[key], lambda new_value: st.session_state.update({key: new_value})

4. Building the Counter Component

To demonstrate the use of these concepts, I built a simple Counter component that uses the use_state hook to keep track of its count value. The component includes two buttons to increment and decrement the count.

class Counter(Component):
    def __init__(self):
        super().__init__()

    def render(self):
        # Use the custom use_state hook to manage count state
        count, set_count = use_state(f"counter_{self.component_id}_count", 0)

        def increment():
            set_count(count + 1)

        def decrement():
            set_count(count - 1)

        # Render the component using VNodes
        return VNode(
            type="container",
            key=f"counter_container_{self.component_id}",
            props={},
            children=[
                VNode(type="text", key=f"count_display_{self.component_id}", props={"content": f"Current Count: {count}"}),
                VNode(type="button", key=f"increment_button_{self.component_id}", props={"label": "Increase", "on_click": increment}),
                VNode(type="button", key=f"decrement_button_{self.component_id}", props={"label": "Decrease", "on_click": decrement})
            ]
        )

5. Handling Re-Renders

One of the biggest challenges was managing re-renders. Streamlit’s top-down execution model means that every interaction re-runs the entire script. I had to ensure that each state change was immediately reflected in the UI by leveraging st.session_state effectively.

Challenges and Solutions

Initially, I encountered issues where state was reset after every interaction. To solve this, I used st.session_state to persist state values and ensure they were not lost during reruns.

Streamlit’s rerun mechanism caused a delay in UI updates. To overcome this, I avoided using unnecessary st.experimental_rerun() calls and relied on session_state to directly update UI components.

Each component needed a unique identifier to keep track of its state. I used UUIDs to generate unique keys for each component instance, ensuring that their state remained independent.

Final Thoughts

Building this mini framework was a great way to understand the intricacies of both Streamlit and React. It gave me a deeper appreciation for how frontend frameworks manage state, handle re-renders, and provide a smooth developer experience.

While this is a basic implementation, it opens up possibilities for creating more complex component-based systems on top of Streamlit. I hope this encourages other developers to explore similar concepts and push the boundaries of what Streamlit can do.

What’s Next?

I plan to continue refining this framework and potentially add more React-like features such as context, props, and even lifecycle methods. If you’re interested, check out the GitHub repository for the code and feel free to contribute or share your thoughts!

1

  1. If you found this helpful, please share it to help others find it! Feel free to connect with me on any of these platforms=> Email | LinkedIn | Resume | Github | Twitter | Instagram 💜