Skip to main content

Working on Stoneware

Stoneware V2 is being rewritten with Reflex. This guide gives a complete overview of how to work on the Stoneware app, the technologies used, and common patterns you'll encounter. It assumes that you have a good understanding of Reflex (if not, see our Reflex guide).

info

TL;DR:

  • Run the app with mono run stoneware-v2
  • We use Tailwind to apply CSS styles
  • We use the shadcn UI toolkit
  • Avoid Reflex components (rx.vstack, rx.center, rx.button)
  • Instead use:
  • All components are available from the html and ui helpers: from stoneware_v2.components import html, ui

Running Stoneware

That part is pretty simple: mono run stoneware-v2.1

It starts the Reflex app and server, usually available at http://localhost:3000 (app) and http://localhost:8000 (server).

It may take a few seconds the first time or after an update. Afterwards, any code changes you make should trigger a terminal message and browser refresh after a few seconds.

Styling with Tailwind CSS

My stance is that Tailwind CSS is the single best way to write CSS.

Here's a great 2 minutes summary:

The core idea is that, instead of writing CSS, you use pre-defined CSS classes to style your elements. Those include pre-defined color palettes, spacing distances, and utilities that go beyond regular CSS.

This allows you to write your styling directly in your HTML (or, in our case, Reflex code), rather than a separate file or location.

Also, a lot of work went into the framework to mitigate the most unintuitive behaviors of CSS. Generally that means that when you apply a style to an element, it only affects that element and nothing else (which is absolutely not the case in regular CSS).

The tradeoff is that you end up with long strings of CSS classes that are hard to read at first - although it quickly becomes a very efficient way of reading the style of an element.

Conclusion: we're using Tailwind. Now, let's dive in.

Learning Tailwind

Tailwind CSS is really popular and has a lot of great resources. I would recommend watching this 12 minutes video to get started.

Beyond that, the official documentation is excellent:

  • You'll find a reference for every class available in Tailwind (ex: padding)
  • Use the sidebar and search heavily, they are well-made and easy to navigate.
  • The Core Concepts provide a deep dive into the mental models behind Tailwind. Not necessary to get started, but very interesting and helpful down the line.
  • Tailwind Play is an interactive playground to try the effect of Tailwind classes

Taking inspiration from existing pages and UI elements in the Stoneware codebase should also prove very helpful.

Like all new tools, it will take some time to get used to Tailwind classes. However, once you do, you will 1) become very efficient at styling web applications and 2) have learned a ton about CSS. I promise it will be worth it. Plus, we have some nice utilities to help you.

Using Tailwind in Reflex

Tailwind comes built-in with Reflex. To apply Tailwind styles, you can pass a class_name prop with Tailwind classes to any HTML element or component that accepts it. Nearly all shadcn components and most of our custom components accept it (you'll get an error quickly otherwise).

info

The class_name prop maps to the class attribute in HTML, used to assign CSS classes to an element. It's called class_name as an artifact from React: class is a reserved keyword in JavaScript, so React had to rename the attribute to className, which becomes class_name in Reflex.

def header_image():
return ui.link(
html.img(
src="/icon.svg",
alt="Brimstone logo",
class_name="h-8 w-8 md:h-10 md:w-10",
),
html.p(
"STONEWARE",
class_name="hidden text-base font-bold text-foreground sm:text-xl md:block",
),
to="/",
class_name="flex items-center gap-4",
)
warning

Do not dynamically create Tailwind classes from variables. Tailwind generates CSS from your code by looking at all the classes found in strings in your code. If you include a variable, it cannot run your code to determine what class is being used.

# ❌ DON'T - Tailwind can't detect these classes
def status_badge(color: str):
return html.span(
"Status",
class_name=f"bg-{color}-500 text-{color}-900" # Won't work!
)

# ✅ DO - Use predefined variants instead
def status_badge(status: str):
return html.span(
"Status",
class_name=rx.match(
status,
("success", "bg-green-500 text-green-900"),
("error", "bg-red-500 text-red-900"),
("warning", "bg-yellow-500 text-yellow-900"),
)
)

Our own Tailwind utilities

We have two utilities to help you work with Tailwind:

  • tw() builds Tailwind classes from keyword arguments, if you prefer that to long strings
  • cn() intelligently merges classes and handles duplicates
from stoneware_v2.components import html
from stoneware_v2.tailwind import cn, tw

def save_button(is_loading: bool = False):
return html.button(
"Save Changes",
class_name=cn(
# `tw` builds Tailwind classes by passing in keyword arguments
tw(
px=4, py=2, rounded="md", font_weight="medium",
bg_color="blue-600", text_color="white",
hover="bg-blue-700"
),
# `cn` allows passing in multiple strings.
"transition-colors duration-200",
# It also allows conditional styling, filtering out falsy values
is_loading and tw(bg_color="gray-200", hover="bg-gray-400")
# Note that classes in later strings take priority, so the background color for `is_loading` will overwrite the background color from the first argument with `tw`.
),
disabled=is_loading
)

tw is there if you find working with long strings of Tailwind classes complicated and prefer using a more Pythonic approach.

TODO: Explaining available arguments

cn is useful for conditional styling (as done above) or defining a default style that can be overriden (used a lot in custom components).

Components

Stoneware doesn't use the built-in Reflex components like rx.vstack, rx.center, rx.button, etc...

Instead, we use:

  • Basic HTML elements
  • Components from the shadcn/ui toolkit, which is very common in modern web dev
  • Other custom components (see reference)
Why these choices?
  • Most Reflex components are based upon Radix Themes, which is close to unmaintained. The Reflex team is also planning on moving them out of the core framework.
  • shadcn/ui is a much more complete UI toolkit, actively maintained, widely adopted by the modern web dev community, and highly customizable
  • The other built-in Reflex components like rx.vstack introduce additional styling that plays well when using only Reflex components, but introduces strange behavior otherwise.

Demos of most components are available when you run the app locally (with mono run stoneware-v2) at http://localhost/demo/components. Once the app is hosted, the demos will be available online as well.

I'm currently working on generating an API reference as well.

Writing pages

Pages in Stoneware V2 are defined in stoneware_v2/pages. In any file within this subfolder, functions decorated with rx.page("/path/to/page") will be automatically detected and added to the site. This is similar to how we define Dagster assets.

For example, the current flowsheets page looks like:

@rx.page("/", on_load=FlowsheetState.load_flowsheets)
@rx.page("/flowsheets/[[...flowsheet]]", on_load=FlowsheetState.load_flowsheets)
@main_layout(full_screen=True)
def flowsheets_page() -> rx.Component:
return html.main(
flowsheet_menu(),
flowsheet_canvas(
flowsheet=FlowsheetState.flowsheet_data,
subflowsheet=FlowsheetState.subflowsheet,
on_select_subflowsheet=FlowsheetState.on_select_subflowsheet,
),
class_name="...",
)

This is a pretty complex example, on purpose. Let's break it down:

  • @rx.page registers this function as a page. You will notice it's used twice, so you can register the same function for multiple URLs this way.
  • The second use of @rx.page includes a path parameter [[...flowsheet]]. This is used elsewhere on the page, see the dynamic routing docs.
  • There is an on_load event handler that triggers after the page is first rendered. This is often used to display the page first, then start loading heavier data. It's also important for protecting data (see Authentication).

Finally, another decorator is used: @main_layout. This decorator adds the navigation bar at the top of the Stoneware website. It should be used on nearly all pages, to ensure a standardized look.

It also takes an optional full_screen argument (default is False). By default, the content on each page is constrained to a standardized width, to make it pleasant to navigate. However, this doesn't work for all pages (ex: flowsheets page, Streamlit tools), so full_screen=True makes the page content take the full width of the screen.

info

To summarize:

  • Use @rx.page("/path/to/page") anywhere in stoneware_v2/pages to register a new page.
  • Use @main_layout on your page.
  • Use @rx.page("...", full_screen=True) if the page needs to take the full width (should be rare).

Patterns

Authentication

One strong point of our Reflex setup is that we can implement authentication and protect data against non-authorized users (contrary to our current Streamlit setup).

The current flow when a user opens the website is:

  • The page loads without any sensitive data (this is really important, we'll come back to it)
  • The page checks if the user is logged in.
  • If not, it redirects to a Microsoft login screen. This login only works for Brimstone accounts and sends the user back to the page they navigated to.
  • Once logged in, the user can access the page and the content starts loading.

After the initial load, this check still runs on every page but passes instantly. However, if the user closes the tab and comes back later, the check will have to load again and the page will show a short loading spinner.

Details

Why a loading spinner every time? Reflex isn't perfect for authentication yet. Regular web apps are often able to detect that a user is logged in before preparing the page, so they can directly display the content if the user is logged in.

That's not the case for us: we need to load the website and only afterwards can we check whether the user is logged in. This results in a short loading spinner every time a user opens the website.

In our case, authentication is mainly used for authorizing users to access Brimstone data. We have two levels of protection:

  1. On each page load, the app checks if the user is signed in. This happens in @main_layout.
  2. For each data loading function, check that the user is logged in.
info

Number 2 is required due to another Reflex quirk. Currently, even if main_layout doesn't display the page content for non logged-in users,

Data fetching

  • Loading indicator
  • Dat afetching examples

Browser development

TODO: introduction to browser dev tools

Advanced

TODO:

  • Adding custom JavaScript / TypeScript

Footnotes

  1. This assumes you have the monorepo tooling set up. If not, see the guide to set up your dev environment.