Advance your ipywidget app development with Reacton — A pure Python port of React for faster development

Logo of Reacton
Reacton: A port of ReactJS to Pure Python

TL;DR

What’s with ipywidgets, anyway?

One of the standout features of ipywidgets is its bidirectional communication capabilities. Every property can be changed from the front end and the Python process, which is a feature that many other frameworks lack. Additionally, ipywidgets is a mature, well-maintained framework backed by a team of talented developers.

Fast forward a few years, and I transitioned from working in astronomy to consulting and freelancing. I helped create ipyvuetify to enable the building of more modern interfaces within the Jupyter notebook. I had the opportunity to work with QuantStack on the development of Voilà. In addition, I contributed to the creation of Glue-Jupyter and Jdaviz. These are applications that can be used both inside and outside of the Jupyter notebook with the help of ipywidgets and ipyvuetify.

This experience led us to the development of Reacton and Solara, which address the limitations of core ipywidgets and enable the creation of more powerful, flexible data apps.

Common challenges when using ipywidgets

What if there was a more intuitive, flexible way to develop these applications? Can we create a better way to approach this from both a developer and client perspective? How can we address these challenges while still using existing libraries?

One way to illustrate these challenges is through a code snippet:

Case 1: A button with counter

import ipywidgets as widgets

clicks = 0 # issue 3
def on_click(button):
global clicks # issue 3
clicks += 1
button.description = f"Clicked {clicks} times" # issue 1
button = widgets.Button(description="Clicked 0 times") # issue 1
button.on_click(on_click) # issue 2
display(button)
A button with a counter: using pure ipywidgets, showing 4 issues

This code can be run in the Jupyter notebook to produce a button. Every time the button is clicked, the label will update to show the number of clicks. This simple example highlights some common problems with using ipywidgets:

  1. The button description text is repeated in two places (initialization and the event handler).
  2. An event handler is attached, but without a defined life cycle, it can be difficult to know when to detach it to avoid memory leaks.
  3. The “clicks” variable is stored in the global scope, which may be concerning for some developers.
  4. The code is not easily reusable or composable.

These issues can be addressed, but it requires the developer to come up with their own solutions. Without a standard approach, different developers may create solutions that are incompatible with each other.

To overcome these challenges, we need a standardized way to build “components” using ipywidgets. This would enable more efficient, flexible development of complex applications.

Case 2: A Markdown Widget/Component

The issue thread includes a suggestion for creating a markdown widget/component using the following code:

import markdown 
from ipywidgets import HTML

html = markdown.markdown("""# Markdown""")
HTML(html)

However, this approach does not provide a clear way to create a reusable Markdown widget. It is unclear whether to inherit from a specific class, or whether to compose a new widget using existing components like VBox or HBox. There is also no obvious way to update the markdown content once the widget has been created.

Without a standard approach, it is likely that different developers will come up with incompatible solutions, making it difficult to build an ecosystem of reusable and composable components.

What can we learn from history and other languages or frameworks?

The ipywidgets ecosystem has not adopted a clear vision of how to build a larger application. Should we consider using a paradigm like ReactJS, but for Python?

When looking for solutions to the challenges discussed earlier, we considered various design patterns like MVC, MVP, and MVVM. However, these patterns often add unnecessary boilerplate and do not provide a clear path forward for building complex applications with ipywidgets.

We also looked at other languages and frameworks and found that ReactJS stands out as a highly effective platform for building web applications. ReactJS is widely used and well-supported and has evolved beyond the traditional DOM manipulation approach.

The ipywidgets ecosystem has not adopted a clear vision of how to build a larger application. Should we consider using a paradigm like ReactJS, but for Python?

Why React?

  • It has proven to be effective and reliable over time
  • There are plenty of resources available to support and enhance its functionality
  • Countless smart brain time has been spent on this JavaScript framework. If we can tap into this resource, we can avoid many roadblocks

What is Reacton?

One key difference between Reacton and ReactJS is that Reacton doesn’t have to worry about browser performance, making Reacton simpler than ReactJS. While implementing Reacton wasn’t easy, the project is relatively small, with under 2000 lines of code, making it easy to understand and work with. Additionally, it has been tested throughout, and the code-base has extensive test coverage.

Are you ready to give Reacton a try?

Show me the money, ehm examples

import reacton
import reacton.ipywidgets as w

@reacton.component
def ButtonClick():
button = w.Button(description="Hi")
return button

Note that we import reacton.ipywidgets as w instead of importing ipywidgets directly. This module wraps all ipywidgets into Reacton widget components.

This code defines a user component, which is decorated with the @reacton.component decorator. When calling the component (i.e. element = ButtonClick()), the function body is not executed immediately. Instead, it returns an element object that holds all information (a reference to the original function and all arguments passed). Reacton will execute the function body at a later time, and will turn the elements into actual ipywidgets: i.e. Reacton is declarative.

Type checking

For example, the following code would generate a type error message:

@reacton.component
def ButtonClick():
button = w.Button(description=1) # description should be a str
return button

The mypy output gives:

error: Argument "description" to "Button" has incompatible type "int"; expected "str"

While type-checking may not be necessary for every project, it can be a useful tool for catching errors and ensuring code quality. Reacton’s type-checking capabilities can help reduce the need for extensive testing and lead to more efficient and productive development.

Autocomplete

Both these options allow you to code faster, with more confidence, with fewer runtime errors, and with your CI checking for type errors — fewer chances of bugs.

Displaying your component

The long answer

# create an element out of the component
element = ButtonClick()
# ask react to render the element into a widget
widget, render_context = reacton.render_fixed(element)
# ask Jupyter to display the widget
display(widget)

The short answer

ButtonClick()

Adding a click event handler

The long answer

@reacton.component
def ButtonClick():
button_element = w.Button(description="Hi")

def attach_event_handler():
# get a reference to the real widget
button_widget = reacton.get_widget(button_element)
def on_click(button):
print("Yes, you clicked")

def cleanup():
button_widget.on_click(on_click, remove=True)
# add the event handler, and return the cleanup
button_widget.on_click(on_click)
return cleanup

reacton.use_effect(attach_event_handler)
return button_element

ButtonClick() # display button in Jupyter

The function passed to use_effect gets called after the render phase, when the widget is created, allowing us to attach our event handler to the real widget, using get_widget. Note that the return value of use_effect should be a cleanup function that will be called when the components are removed. In our case, we detach the event handler. This ensures that there are no memory leaks. Another important note: the reacton.get_widget function should only be called inside of the use_effect handler, not elsewhere.

This pattern of using use_effect and its cleanup function allows for robust connection between components and external systems, without memory leaks.

Short answer

@reacton.component
def ButtonClick():
# this handler can also be defined outside of the component
# but it's considered idiomatic define it locally
# especially if the event handler will not be used outside
# of this component
def my_click_handler():
print("Yes, you clicked")
button = w.Button(description="Hi", on_click=my_click_handler)
return button
ButtonClick()

In addition, Reacton also supports event handlers for each property in ipywidgets. For example, if a widget exposes the property “value”, we can also bind an on_value callback to listen for changes.

Adding state

@reacton.component
def ButtonClick():
# first render, this return 0, after that, the last argument
# of set_clicks
clicks, set_clicks = reacton.use_state(0)
def my_click_handler():
# trigger a new render with a new value for clicks
set_clicks(clicks+1)
button = w.Button(description=f"Clicked {clicks} times",
on_click=my_click_handler)
return button
ButtonClick()

In this example, the initial render will show the button with a description of “Clicked 0 times”. When the button is clicked, the my_click_handler function is called, which updates the value of clicks by calling set_clicks(clicks+1). This triggers a re-render, and the button will now show “Clicked 1 times”.

State management in Reacton follows the same principles as ReactJS, with only the component that creates the state being able to modify it. This ensures that there is no global state that can be mutated from arbitrary locations.

You can also try out this example directly using Jupyter Lite

Hooks? Hooks!

Let's take a look at how this can go wrong.

@reacton.component
def MyFilter(df):
# first call to use_state
column, set_column = reacton.use_state(str(df.columns[0]))
# second call to use_state
if is_numeric_dtype(df[column]):
min_value, set_min_value = reacton.use_state(0)
else:
picked_value, set_picked_value = reacton.use_state("Dog")
# oops, now min_value and picked_value refer to the same 'state'
....

In the example above, calling use_state(0) on the first render sets our second "state slot" to an integer with value 0. If the component re-renders and triggers the use_state("Dog") code path, picked_value will be set to the integer 0! This is because it is the second call to use_state. To fix this, we can simply remove the condition and call use_state for both the integer and the string versions.

While they may not seem significant at first, hooks are incredibly powerful. They are composable, meaning you can call hooks within other hooks, as long as they are not called conditionally. By putting most of your non-UI logic in hooks, you can greatly simplify your UI components and reuse your hooks throughout your code.

The Markdown Component

import markdown

@reacton.component
def Markdown(md: str):
html = markdown.markdown(md)
return w.HTML(value=html)

Markdown("# Reacton rocks")

Although there may be a variety of Markdown libraries that users may choose from, the ability to reuse components, in the same way, ensures a consistent and efficient solution. For instance, when creating a Markdown editor, all users can utilize the Markdown component in the same way.

@reacton.component
def MarkdownEditor(md : str):
md, set_md = reacton.use_state(md)
with w.VBox() as main:
w.Textarea(value=md, on_value=set_md)
Markdown(md)
return main

display(MarkdownEditor("Mark-*down* **component**"))

Understanding the flow of data is key to understanding how Reacton works. Data flows up using an event handler (on_value will trigger set_md, which will change md in the next render phase). Data then flows down via argument; md gets passed down to the TextArea and Markdown components.

Containers as context managers

@reacton.component
def MarkdownEditor(md : str):
md, set_md = reacton.use_state(md)
return w.VBox(children=[
Markdown(md),
w.Textarea(value=md, on_value=set_md)
])
display(MarkdownEditor("Mark-*down* **component**"))

This approach works well, but if you have a component with many nested containers, it can become difficult to manage the square bracket notation. Instead, we can use an element (such as a VBox container) as a context manager, and all elements created that are not explicitly assigned to another element will be added to its children.

In principle, normal ipywidgets could support this, but it would do an assignment of the children after the creation of the VBox, which would result in a flicker (first showing an empty VBox, then with its children). This is another example of where a declarative system is superior to an imperative one.

Using an element as a context manager in this way can help to simplify and improve the structure of your code, particularly when working with complex, nested components. It also provides a more intuitive and straightforward way to add child components to a container, making it easier to manage and maintain your code.

Conditional rendering

@reacton.component
def MarkdownEditor(md : str):
md, set_md = reacton.use_state(md)
edit, set_edit = reacton.use_state(True)
with w.VBox() as main:
Markdown(md)
w.ToggleButton(description="Edit",
value=edit,
on_value=set_edit)

if edit:
w.Textarea(value=md, on_value=set_md, rows=10)

return main
display(MarkdownEditor("Mark-*down* **component**"))

On the first render, edit is True, so all components are rendered. The on_value event handler is also attached to the TextArea component. If we toggle the "Edit" button, changing edit to False, Reacton calls the component's render function again and detects a difference between the previous render phase and the current one. During what is called the reconciliation phase, Reacton applies the necessary changes to the widgets to bring their state up to date. In this case, it will:

  • Change the ToggleButton’s value to False
  • Remove the TextArea as a child of the main VBox
  • Remove the on_value event handler from the TextArea
  • Destroy the TextArea widget

Manually performing these tasks without the help of a framework like Reacton would be tedious and error-prone. This type of “conditional rendering” is something that developers typically avoid without the assistance of a framework like Reacton. With Reacton, it becomes simple and straightforward.

What is next

A common library?

While we both like to see a common codebase, this definitely won’t be a walk in the park.

A fairly manageable task, however, is to provide a shared package for hooks that will dispatch to the currently used implementation. This allows us to use a hook written for Reacton to be used in IDOM, and vice versa.

Once a common library for rendering/reconciliation exists, the same procedure can be repeated for Qt or other libraries, possibly Bokeh or Textual.

Learning

The starting point for Reacton is the Reacton-documentation, however, there is a much larger resource available. A lot can be learned from the JavaScript world. For instance, if you want to know how to use use_reducer and use_context, you could start by searching on YouTube and following some ReactJS tutorials. Code-wise you should mentally translate from pascal casing (useReducer) to snake case (use_reducer).

The vast amount of information available on ReactJS is one of the biggest advantages of using Reacton. YouTube, StackOverflow, books, and personal blogs are all great places to find valuable resources on ReactJS and related topics. However, you don’t have to rely solely on external resources to learn about Reacton. The Reacton website includes examples and basic documentation, including information on hooks, which you can use to get started:

Reacton documentation

Or go directly to one of our examples:

Click the link/image to try the online example.
Click the link/image to try the online example.
Click the link/image to try the online example.

Conclusions and the Future

Reacton is open source, MIT licensed, and our git repo is on GitHub.

Visit our GitHub repo
Visit our GitHub repo at https://github.com/widgetti/reacton

But that’s not all. We are currently developing a new framework called Solara, which combines the power of Reacton, the material design of ipyvuetify, and a new deployment server. Solara makes it easy to create powerful web apps with Python, using proven technology (React/Reacton) and multi-page capabilities both inside and outside Jupyter notebooks.

If you are interested in Reacton and Solara, we are currently running a beta program. We are still refining the API, so access is limited to a select group of users. If you would like to try Solara and provide feedback, please let us know how Solara can solve your problem. We will give you access to the beta program and invite you to our Slack channel to collect feedback from a small group of users. As an added bonus, we are offering free consultancy in the form of a video call to anyone who can describe their use case in an interesting way.

Sign up for the Solara beta program, newsletter, or let us know your use case to learn more by clicking the logo below.

Interested in Reacton, and curious about Solara? Sign up here.

Or follow me on Twitter at https://twitter.com/maartenbreddels

--

--

Data Science tooling for Python, creator of Reacton, Solara, Vaex, ipyvolume

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Maarten Breddels

Data Science tooling for Python, creator of Reacton, Solara, Vaex, ipyvolume