The Provider-Local Class Pattern
So you want to use React with iModel.js? Great, React is a very popular web framework for very good reasons, it has a huge following, a huge community and tons of open source packages to build upon. How to deal with classes in React will be important to us in an iModel.js app, since iModel.js is chock-full of framework-agnostic vanilla TypeScript classes. In a previous article, Writing a Primitive Tool, I built a microscopic iModel.js app, which I will improve here by applying the Provider-Local Class pattern.
This is my favorite pattern for using vanilla JavaScript classes in React.
We'll also discuss the merits of Redux and why both are used in React-land.
The problem with React is that it is a framework; it is opinionated, so integrating external concepts requires folding them into React’s primitives. In particular, state in React is immutable and mutations must be made through specific dispatchers (setState
, useState
) that enqueue updates and trigger renders. Top-level classes on the other hand can have mutable state changing willy-nilly without anyone noticing, and will need to be given indirect references and accessors to the latest immutable state in React, which is especially horrible with the introduction of React Hooks as we'll see.
To inspect this thoroughly, we’ll start where the previous article leaves off, with a “finished” mini-iModel.js app. It is hosted in my fork of the iModel.js basic viewport app sample, and we left off at this particular commit. Every time I reach a good change point, I will mention and link a corresponding commit, so you can use GitHub’s interface to read the changes and also see the full code at each point.
Back to the App
In the previous article, we built a tiny iModel.js app for our users to pin locations in the model, and manage those pins.
It’s simple, and didn’t involve much new code for the sample (you can check out previous commits in the fork). Anyway, let’s take a look at the code for our Pin tool and accompanying marker.
To get the pins in our classes, we need to pass an accessor getPins
down through props and eventually to the initialization of our object, in our case in run
. This is fine here, in a small example, but when we start integrating multiple data sources as we scale the feature-depth of our logic, we start needing more and more accessors to more and more sources of data.
It’s much worse if our App
component was a functional component using hooks. Consider this code:
This won’t work. Each time the app is rendered, the entire function is reran so the scope is unique. So when the PlacePin
tool inevitably calls getPins
, it points only to the state of pins
at that point in time. If it were to change before that, the getPins
function that PlacePin.run
received would still be the old function passed to it, and would just get the value of pins it captured back then, giving us a bug. getPins
in the class component worked because our class gave us an indirect reference to our state properties. () => this.state.pins
only captures the this
reference. It then accesses the value of the "state"
property on the object referenced by this
, and finally the "pins"
property on that. In the function scope when we get our state from React.useState
, we have no indirect object, unless we make one. Thus the solution is this ugly thing:
Now, pinRef.current
will be updated, and the actual access will occur indirectly through it, giving us an up-to-date state reference. But I digress, yes functional components make this problem even uglier, but it's the same problem that class components have, meshing complex React state with top-level classes doesn't scale. If we're using a functional component we'll need a new ref and effect for every state item we want to access. Even if we crawl back to class components, we'll still need a new accessor on our iModel.js class, a place to set the accessor (often on-mount logic), and we'll have to handle the undefined
case since React initialization is not guaranteed to happen before the class is constructed¹. But there are solutions.
The mainstream response to the problem of complex state in the React world is to centralize our state, this is Redux
's approach. While Redux is a complete solution, it comes with its own weight, lots of boilerplate (especially when using TypeScript), a learning curve, and interfacing with external state requires writing more boilerplate (same as React really). In modern React, with the addition of the React.useReducer
hook and the React Context API, Redux is no longer as much of a requirement as it used to be, so it would be nice if we didn't need it. But then, how do we have our iModel.js classes access complex application state without centralizing it?
If only there were a way to access React state as if we were programming in a component, then most of the redundancy could go away. Therein lies the answer. If we free ourselves from the restrictions of a module-level class and instead define our class inside a component, then that local class can trivially access React’s state in its implementation. All that’s left to do: we need to provide the local iModel.js class to other components so they can use it, which they usually need to. For example: in our App’s Toolbar component, we need the PlacePin
class.
This is the Provider-Local Class pattern.
Let’s rewrite PlacePin
to be local to App
and watch the glue disappear, and then finally separate it from App
too. To begin, let's delete the PlacePin
tool implementation from Pin.tsx
and instead add it to the App
component as a property, and initialize it on mount:
Notice that inside the local class we can’t access the this
reference which our property normally can, so we have to quickly wrap the property in a preparing function that captures an alias to the component's this
. Still, our run
function and the redundant accessor properties disappear completely, as well as App
's rendering becoming simpler since it also doesn't need to pass state glue, if you had multiple contexts to check, like theme contexts, authorization contexts, GraphQL state, external redux stores, this would offer an even stronger clean up, and it already cleans up well on this contrived example.
In the app, we had to pass getPins
and setPins
down through props to the Toolbar
component that displays the existing tools. Now we can remove those props in favor of a single placePinTool: typeof Tool
prop. You can see the diff for these additional changes. We won't need this prop at all once we start correctly providing our classes to the application.
Now, if you were to run the application after these changes, you would notice that the pin markers no longer show up. This is because we’re no longer initializing our pinDecorator
the way we were before, because we deleted the run
method. Instead, we'll make the entire pinDecorator
also local so that it can directly get the pins itself when it needs them. In fact, let's bring our PlacePin
class back to Pin.tsx
, and decouple the implementation from App
.
To do this, we need a component scope to provide the Pin implementation, let’s rewrite Pin.tsx
's classes to be completely contained within a PinProvider
component. Since the modern way to implement the Provider pattern is using React's aforementioned Context API, we'll do it that way.
Indeed, this is a bit wordier, but it’s far from verbose, and in fact we get to remove initialization references from the BasicViewportApp.ts
file that previously we needed without being able to use React's own initialization patterns. This is just idiomatic React, now we can render this component and it is debuggable by the same simple, functional rules of React. Also, any nested child in the provider's tree can use the classes easily. As long as we place this provider early in our structure (exactly like a centralized state store would be placed), it is always available and references are stable. For any class that needs to be aware of UI state most naturally accessed in React, this pattern makes it very easy. If you wish it weren't a class component, know that I do too. I spent a decent amount of time working on a useClass
hook but ran into several problems.
Let’s take a look at how our Toolbar
component would look when consuming the context without using any prop passing:
All we need to do is have App
wrap its children with the provider when rendering. You can see all prop passing changes in the final revision.
We don’t even need to wrap it around everything, we could have just wrapped it around the {ui}
fragment. And bam! We just implemented our first Provider-Local Class pattern.
Hopefully you can see some of the usefulness in this pattern, with the ability to tie in to React's own lifetime management, we can fearlessly use our iModel.js classes with any React library we want to throw at it. That's about it for demonstrating the pattern, but as a bonus, below I added a contrived example of how you could trivially use tons of for-React packages in your tools using this pattern.
Please discuss, offer alternatives, share your experiences, and present corrections to any of the content on the iModel.js discussions page or send me an email at mikemikeb@protonmail.com. Thank you for reading!
Features at Scale:
Suppose we needed to use our own contexts, some third-party state hooks, GraphQL, and react-router
to manage our own application's navigation state. Using the new local class you can direct all of your state idiomatically through the component tree like so:
Even hooking up several external Redux stores wouldn’t be that bad given this, using anything managed in React land becomes trivial in your classes. Thanks again.
Footnotes:
- In our solution (the Provider-Local Class pattern) this actually will be guaranteed!