Mapping declarative React components to imperative external API. An example based on seats.io library.
Sometimes your React components are wrappers for external components. And those external components APIs might not be declarative. Instead, they provide you with a set of imperative APIs that you can use to change their state. What can you do in such situation?
I recently stumbled upon this problem when integrating our React frontend
with seats.io
library. The library renders venue map and provides you with
callbacks when certain seats are selected or deselected. It
also updates the venue map via WebSockets. When seats are
taken or released by other users our map reflects it.
I’ve been happy with the results. But like I said, their API is imperative. It has methods such as:
clearSelection()
- Unselect currently selected seatssetUnavailableCategories(list)
- Takes an array of category keys (e.g.["VIP", "Left sector"]
`). Use this if you want to disable categories, e.g. per user role, a moment in time, and whatnot.
And similar…
Mounting is easy
import { isEqual } from 'lodash';
import React, { Component } from 'react';
const DIV_ID = 'venue_map';
class VenueMap extends Component {
componentDidMount() {
this.seatingChart = new seatsio.SeatingChart({
divId: DIV_ID,
publicKey: this.props.publicKey,
event: this.props.eventKey,
onObjectSelected: this.props.seatSelectedHandler,
onObjectDeselected: this.props.seatDeselectedHandler,
unavailableCategories: this.props.unavailableCategories,
selectedObjects: this.props.selectedObjects,
});
this.seatingChart.render();
}
render() {
return <div id={DIV_ID}></div>;
}
}
As you can see providing a list of selected seats and unavailable categories
is easy when the venue map is initially rendered. We can use unavailableCategories
and selectedObjects
config options. But there are use-cases where
these two change in run time.
When a sale is finished and POS system is ready to handle the new order,
we want a clear list of selected seats. We should call clearSelection()
.
Or when the user is a VIP and knows a secret that unlocks part of the venue which is
unavailable. We should use setUnavailableCategories()
to unlock it.
componentWillReceiveProps(object nextProps) to the rescue
componentWillReceiveProps
-
Invoked when a component is receiving new props. This method is not called for the initial render. Use this as an
opportunity to react to a prop transition before render()
is called.
componentWillReceiveProps(nextProps) {
if (nextProps.bookingId !== this.props.bookingId){
this.seatingChart.clearSelection();
}
if (!isEqual(nextProps.unavailableCategories, this.props.unavailableCategories)){
this.seatingChart.setUnavailableCategories(nextProps.unavailableCategories);
}
}
componentWillReceiveProps
lifecycle method gives you an access to current properties
via this.props
. You also have an access to properties which are going to be used in a moment during the next render
via nextProps
. This gives us the ability to compare both of them and react. For example by calling an appropriate
imperative API.
In our case, we check if bookingId
didn’t change. It’s an identifier for booking the places selected on the VenueMap
.
If it changed, it means we are processing a new order and the seats need to
be cleared.
And for the categories, we compare unavailableCategories
. If it changed we call setUnavailableCategories
API. Or, if there is no negative performance implication, we can always call the method.
componentWillReceiveProps(nextProps) {
if (nextProps.bookingId !== this.props.bookingId){
this.seatingChart.clearSelection();
}
this.seatingChart.setUnavailableCategories(nextProps.unavailableCategories);
}
It really depends on the library you are integrating with, and the specific use-case you have in your app.
Imperative programming imposed
Once you have to deal with an imperative API there is no easy to avoid it. It must affect your code in some places. However, you can keep it mostly hidden from your declarative API.
Sometimes you don’t have a choice and you need to cooperative with given library. Like in my case of
seats.io
integration that we have. But sometimes you can do yourself a favor and look for a library
which is compatible with React way.
A few days ago I refactored a React wrapper for a modal library with
an imperative API. I moved to react-modal
and I was satisfied with
the number of lines removed.
Remember to clean up
If you read The real reason to avoid jQuery
then you know it. Appplications’ performance often suffers because of libraries which don’t clean up after themselves.
So be a good developer and remember to use componentWillUnmount
to clean up the integration.