<RETURN_TO_BASE

Build a Real-Time Multi-Page Reflex Notes App in Python — No JavaScript Required

Step-by-step guide to build a multi-page Reflex notes dashboard with real-time database interactions and a reactive UI, all in Python.

Why Reflex and what this tutorial builds

This tutorial walks through building an advanced Reflex web application entirely in Python and running it inside Google Colab. The project demonstrates how Reflex enables full-stack development without JavaScript by using reactive Python code for both backend and frontend. The final app is a multi-page notes dashboard with a persistent SQLite database, real-time updates, filtering, sorting, analytics, and user personalization.

Setup in Colab

Start by creating a project folder and installing Reflex. The snippet below prepares the environment and installs a specific Reflex version:

import os, subprocess, sys, pathlib
APP = "reflex_colab_advanced"
os.makedirs(APP, exist_ok=True)
os.chdir(APP)
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "reflex==0.5.9"])

This creates a directory for the project, changes into it, and installs Reflex so the rest of the tutorial can run smoothly inside Colab.

Minimal configuration

Define a minimal configuration file that tells Reflex the app name and database connection. The configuration below writes a small rxconfig.py referencing a local SQLite database:

rxconfig = """
import reflex as rx
class Config(rx.Config):
   app_name = "reflex_colab_advanced"
   db_url = "sqlite:///reflex.db"
config = Config()
"""
pathlib.Path("rxconfig.py").write_text(rxconfig)

Keeping the config minimal makes it easier to focus on the app logic while still enabling persistent storage for notes.

Data model and reactive state

Next, define the data model Note and the reactive State class that drives the UI and database interactions. The State includes fields for user input, search, tag filtering, sorting, and toast messages, and asynchronously manipulates the database.

app_py = """
import reflex as rx
 
 
class Note(rx.Model, table=True):
   content: str
   tag: str = "general"
   done: bool = False
 
 
class State(rx.State):
   user: str = ""
   search: str = ""
   tag_filter: str = "all"
   sort_desc: bool = True
   new_content: str = ""
   new_tag: str = "general"
   toast_msg: str = ""
 
 
   def set_user(self, v: str): self.user = v
   def set_search(self, v: str): self.search = v
   def set_tag_filter(self, v: str): self.tag_filter = v
   def set_new_content(self, v: str): self.new_content = v
   def set_new_tag(self, v: str): self.new_tag = v
   def toggle_sort(self): self.sort_desc = not self.sort_desc
 
 
   async def add_note(self):
       if self.new_content.strip():
           await Note.create(content=self.new_content.strip(), tag=self.new_tag.strip() or "general")
           self.new_content = ""; self.toast_msg = "Note added"
 
 
   async def toggle_done(self, note_id: int):
       note = await Note.get(id=note_id)
       if note:
           await note.update(done=not note.done)
 
 
   async def delete_note(self, note_id: int):
       await Note.delete(id=note_id)
       self.toast_msg = "Deleted"
 
 
   async def clear_done(self):
       items = await Note.all()
       for n in items:
           if n.done:
               await Note.delete(id=n.id)
       self.toast_msg = "Cleared done notes"
 
 
   async def notes_filtered(self):
       items = await Note.all()
       q = self.search.lower()
       if q:
           items = [n for n in items if q in n.content.lower() or q in n.tag.lower()]
       if self.tag_filter != "all":
           items = [n for n in items if n.tag == self.tag_filter]
       items.sort(key=lambda n: n.id, reverse=self.sort_desc)
       return items
 
 
   async def stats(self):
       items = await Note.all()
       total = len(items)
       done = len([n for n in items if n.done])
       tags = {}
       for n in items:
           tags[n.tag] = tags.get(n.tag, 0) + 1
       top_tags = sorted(tags.items(), key=lambda x: x[1], reverse=True)[:5]
       return {"total": total, "done": done, "pending": total - done, "tags": top_tags}
"""

This code sets up asynchronous CRUD operations, filtering logic, sorting behavior, and statistics calculation that update reactively as the data changes.

UI components and composition

Build modular UI components that directly reflect the reactive state: a sidebar with navigation and a user input, stats cards, tag pills, a notes list, and individual note rows.

app_py += """
def sidebar():
   return rx.vstack(
       rx.heading("RC Advanced", size="6"),
       rx.link("Dashboard", href="/"),
       rx.link("Notes Board", href="/board"),
       rx.text("User"),
       rx.input(placeholder="your name", value=State.user, on_change=State.set_user),
       spacing="3", width="15rem", padding="1rem", border_right="1px solid #eee"
   )
 
 
async def stats_cards():
   s = await State.stats()
   return rx.hstack(
       rx.box(rx.text("Total"), rx.heading(str(s["total"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
       rx.box(rx.text("Done"), rx.heading(str(s["done"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
       rx.box(rx.text("Pending"), rx.heading(str(s["pending"]), size="5"), padding="1rem", border="1px solid #eee", border_radius="0.5rem"),
       spacing="4"
   )
 
 
def tag_pill(tag: str, count: int = 0):
   return rx.badge(
       f"{tag} ({count})" if count else tag,
       on_click=State.set_tag_filter(tag),
       cursor="pointer",
       color_scheme="blue" if tag == State.tag_filter else "gray"
   )
 
 
async def tags_bar():
   s = await State.stats()
   tags = [("all", s["total"])] + s["tags"]
   return rx.hstack(*[tag_pill(t[0], t[1]) for t in tags], spacing="2", wrap="wrap")
 
 
def note_row(note: Note):
   return rx.hstack(
       rx.hstack(
           rx.checkbox(is_checked=note.done, on_change=State.toggle_done(note.id)),
           rx.text(note.content, text_decoration="line-through" if note.done else "none"),
       ),
       rx.badge(note.tag, color_scheme="green"),
       rx.button("", on_click=State.delete_note(note.id), color_scheme="red", size="1"),
       justify="between", width="100%"
   )
 
 
async def notes_list():
   items = await State.notes_filtered()
   return rx.vstack(*[note_row(n) for n in items], spacing="2", width="100%")
"""

Using rx.vstack, rx.hstack and rx.suspense allows the UI to compose pieces declaratively while keeping each element directly bound to the reactive state.

Pages, routing and running the app

Assemble the dashboard and board pages, register routes with the app, compile, and run:

app_py += """
def dashboard_page():
   return rx.hstack(
       sidebar(),
       rx.box(
           rx.heading("Dashboard", size="8"),
           rx.cond(State.user != "", rx.text(f"Hi {State.user}, here is your activity")),
           rx.vstack(
               rx.suspense(stats_cards, fallback=rx.text("Loading stats...")),
               rx.suspense(tags_bar, fallback=rx.text("Loading tags...")),
               spacing="4"
           ),
           padding="2rem", width="100%"
       ),
       width="100%"
   )
 
 
def board_page():
   return rx.hstack(
       sidebar(),
       rx.box(
           rx.heading("Notes Board", size="8"),
           rx.hstack(
               rx.input(placeholder="search...", value=State.search, on_change=State.set_search, width="50%"),
               rx.button("Toggle sort", on_click=State.toggle_sort),
               rx.button("Clear done", on_click=State.clear_done, color_scheme="red"),
               spacing="2"
           ),
           rx.hstack(
               rx.input(placeholder="note content", value=State.new_content, on_change=State.set_new_content, width="60%"),
               rx.input(placeholder="tag", value=State.new_tag, on_change=State.set_new_tag, width="20%"),
               rx.button("Add", on_click=State.add_note),
               spacing="2"
           ),
           rx.cond(State.toast_msg != "", rx.callout(State.toast_msg, icon="info")),
           rx.suspense(notes_list, fallback=rx.text("Loading notes...")),
           padding="2rem", width="100%"
       ),
       width="100%"
   )
 
 
app = rx.App()
app.add_page(dashboard_page, route="/", title="RC Dashboard")
app.add_page(board_page, route="/board", title="Notes Board")
app.compile()
"""
pathlib.Path("app.py").write_text(app_py)
subprocess.run(["reflex", "run", "--env", "prod", "--backend-only"], check=False)

This final sequence compiles the Reflex app, registers routes, and starts the backend so the app becomes interactive.

What you get and how to extend it

  • A fully Python-defined frontend and backend powered by Reflex reactivity.
  • Persistent storage using SQLite and model-driven CRUD operations.
  • Reactive filtering, sorting, tag-based grouping, and analytics cards driven by live state.
  • Clear separation between state logic and UI components for easy extension.

You can extend this foundation with authentication, remote databases, richer UI components, or deploy to a dedicated server. The structure shown is suitable for rapid prototyping and teaching how reactive full-stack Python apps can replace parts of traditional JS stacks.

🇷🇺

Сменить язык

Читать эту статью на русском

Переключить на Русский