22
No-el: Eliminate explicit calls to createElement() when using Python to code React applications
For those of you that have been utilizing the approach to creating React applications with Python from the React to Python book, it may feel a bit kludgy having to call the React.createElement()
function all of the time. But doing so is a necessity since JSX isn't a very practical option when using Transcrypt. Or perhaps having all of those el()
's littering your code just makes things look a bit messier than you would prefer. Well, I may have another option for you to try out that eliminates all of those createElement()
calls, yet doesn't really change the way you've been writing your Python React code all that much.
Recently, a colleague was showing me some code they had written in Jetpack Compose for Web, a toolkit by JetBrains which is based on Google’s platform for building reactive user interfaces with Kotlin. It's not React, but it uses essentially the same paradigms as React. One of the features that stood out to me when I was looking at their code was that, unlike React, the native HTML components were represented as actual component functions. So, instead of creating React HTML elements like this as we have been doing in Python:
el('div', {'style': {'padding': '12px'}}, "Hello React!")
where the HTML element is represented as a string, Jetpack Compose for Web treats HTML elements as first-class citizens and uses something more like this:
Div({style {padding(12.px)} }) {Text("Hello Jetpack!")}
Seeing that got me thinking: "I wonder how difficult it would be to utilize that type of syntax in my own Python React projects?" Going into it, I knew that I would want to accomplish two things in this challenge:
- Create wrapper functions for HTML elements so that React HTML components would be represented like any other functional React component.
- Create a Python decorator that wraps component functions with the call to
createElement()
.
Accomplishing these two goals would effectively eliminate the need to explicitly use createElement()
calls for generating every React element in a component tree. Well, I'm happy to say that I accomplished both of those goals. And it turns out that it wasn't even that difficult to do (I really do love Python!).
If you recall from the React to Python book or the Creating React Applications with Python tutorial, a module called pyreact.py is created to hold all of the code that bridges Python objects to the React JavaScript library. It turns out that we only need to add a few more functions to this module to be able to eliminate all of the calls to createElement()
in the rest of our Python code.
The heart of the entire solution is a single Python function that wraps a React component in a call to createElement()
and returns that as a new function. It looks like this:
def react_component(component):
def react_element(props, *children):
return createElement(component, props, *children)
return react_element
If you have ever created a Python decorator, you may recognize the structure of this function. In general terms, it is a function that takes a function as an argument and then returns a new function. In this case, that returned function takes two (or more) arguments: props
and zero or more children
. The return value of the new function is just the call to React.createElement()
that is used in the usual way.
We will use this function in two ways. Even though this function is structured like a decorator, there is nothing to keep us from also calling it directly. Thanks to the dynamic nature of Python, the component
that we pass into this function doesn't even necessarily have to be a function. In fact, it can even be a string. This feature allows us to handle the first part of our challenge in turning React HTML elements into functional components :
Div = react_component('div')
The string 'div'
that is passed into the function gets used as the first parameter in the call to createElement()
in the generated function. This is just like we were previously using it in our own Python React code. We then save the return value of the call to react_component('div')
in a variable called Div
which now contains the newly generated wrapped function.
Putting all of this together, the additional code we end up adding to the updated pyreact.py module then looks like this:
def react_component(component):
def react_element(props, *children):
return createElement(component, props, *children)
return react_element
Form = react_component('form')
Label = react_component('label')
Input = react_component('input')
Ol = react_component('ol')
Li = react_component('li')
Button = react_component('button')
Div = react_component('div')
Span = react_component('span')
As you can see, creating function wrappers for other React HTML elements becomes a trivial task. Now that we have that in place, we'll next take a look at how these new functions simplify the code we use to create our React applications by eliminating the need to explicitly call createElement()
every time ourselves.
Starting with the React code from the tutorial, we add the necessary wrapper functions to pyreact.py as above and then refactor the demo application to remove the calls to createElement()
.
Listing 1: app.py
from pyreact import useState, render, react_component
from pyreact import Form, Label, Input, Ol, Li
@react_component
def ListItems(props):
items = props['items']
return [Li({'key': item}, item) for item in items]
def App():
newItem, setNewItem = useState("")
items, setItems = useState([])
def handleSubmit(event):
event.preventDefault()
# setItems(items.__add__(newItem))
setItems(items + [newItem]) # __:opov
setNewItem("")
def handleChange(event):
target = event['target']
setNewItem(target['value'])
return Form({'onSubmit': handleSubmit},
Label({'htmlFor': 'newItem'}, "New Item: "),
Input({'id': 'newItem',
'onChange': handleChange,
'value': newItem
}
),
Input({'type': 'submit'}),
Ol(None,
ListItems({'items': items})
)
)
render(App, None, 'root')
The first thing you may notice about this refactored code is that there are no calls to createElement()
in sight! Removing all of those el()
's from the original version of app.py has cleaned up the code quite a bit.
Now that we're not basing the HTML components on strings, we do have to import the ones we use in the module as we did here.
from pyreact import Form, Label, Input, Ol, Li
In the import
line above that one, we also imported the new react_component()
function that we created in the pyreact.py module. Here, we now use this as a decorator for any React functional components that we create. When we do, they will also get wrapped by the call to createElement()
when the component gets rendered by React.
@react_component
def ListItems(props):
items = props['items']
return [Li({'key': item}, item) for item in items]
A side-benefit of using this decorator, is that it now becomes very easy to tell which of your Python function definitions are React components and which are just plain functions.
In this code block, you can also see the first use of our new HTML components that we use in place of the explicit call to createElement()
. So instead of using
el('li', {'key': item}, item)
where the HTML element is identified by a string as we did before, we now use
Li({'key': item}, item)
where the HTML element is a functional component itself.
The other changes we made were all in the return statement that builds the component tree. Here, all of the calls to createElement()
were replaced by their functional component counterparts. All of the props and the child component structure aspects remained exactly the same as they were before making the changes.
For instructions on preparing the development environment and running the code we discussed here, you can reference the original tutorial.
While it is definitely subjective, for me, calling React.createElement()
when aliased as el()
in my Python code is but a small concession for not having JSX available to use in the Python modules. In all honesty, it never really bothered me all that much. But for larger components, having all of those el()
's cluttering up the Python code can tend to impact readability a bit. By encapsulating the call to createElement()
in the component itself, we can avoid having to explicitly call it in our Python code when building the element tree.
One drawback to eliminating the explicit calls to createElement()
is that it might not be quite as evident as to what parts of your Python code are creating React elements versus just making regular function calls. Another possible downside might be that this encapsulation of the element creation could be seen as moving away from the mental model of how React actually works. These points are both very subjective, so you will have to decide for yourself if it makes sense to use what I've described here.
On the plus side, now that we've eliminated some of the createElement()
code clutter, I feel that this approach makes it even less of an issue that we are not using JSX for creating React applications. Additionally, by using defined functions instead of strings for creating HTML components, the IDE is able to help us out a little bit better when coding.
Source Code:
https://github.com/JennaSys/rtp_demo/tree/no-elOriginal Source Code:
https://github.com/JennaSys/rtp_demoTranscrypt Site:
https://www.transcrypt.orgJetpack Compose for Web:
https://compose-web.ui.pages.jetbrains.teamCreating React Applications with Python tutorial:
https://leanpub.com/rtptutorialReact to Python Book:
https://pyreact.com
22