Introduction
In this blogpost I would like to take you on my journey through TypeScript and React and share some really cool features and interesting gotcha's I found along the way. Let's start off with some background information first:
At Moxio we've been using our own scratch-built re-usable component library for years. It has sadly proven to be quite a steep learning curve for our new developers, and sometimes it isn't quite as performant as we would like. We've been looking at TypeScript and React for a while now, and about 6 months ago we bit the bullet. These technologies look stable and performant, and they've been around for a while now and definitely don't seem on the way out.
Our experience with React has been really good. Performance is off the charts, I really like the JSX syntax and the children construct, something we have struggled with in our own component library. I also learned many new things along the way, I have tried to select the most relevant and useful information below.
TypeScript
Support for classes, interfaces, strongly typed variables, parameters and more! it's like a dream come true for me, who more than a decade ago started with object-oriented programming in PHP 5.
There is support for optional properties by using the ?
symbol, support for union types using the |
operator, and many other options, check out variable-declarations and interfaces for a complete overview.
interface HighlightProps {
highlight: string;
test: string;
multiline?: boolean;
type: "strong" | "background" | "cursive";
}
Discriminated unions
One of my favorite constructs is the discriminated union, with which you can use a flag to know which variables are expected. In our first use case we had a flag that allowed item selection, but if it was true we also needed a selectedItem
prop (if false we didn't need that prop), discriminated union to the rescue! The code below defines enableItemSelection prop as a boolean and when it's true it requires the additional selectedItem prop. TypeScript understands this concept and will give you correct autocompletion and validation for it.
interface EnableItemSelection {
enableItemSelection: true;
selectedItem: identifier;
setSelectedItem: (identifier: identifier) => void;
}
interface DisableItemSelection {
enableItemSelection: false;
}
interface CommonProps {
enableItemSelection: boolean;
}
type ComponentProps = CommonProps & (EnableItemSelection | DisableItemSelection);
Spread operator
Another cool thing you can use is the spread operator, which is a very powerful tool for object manipulation. The main use for this syntax is spreading out all the properties of an object, this can be really useful when calling components in React. See the TypeScript spread documentation for more information or check out the JavaScript way of doing it.
const props = {
"cellsCssClass": this.props.cellsCssClass,
"columns": this.props.columns
"getCells": this.props.getCells,
"enableItemSelection": false,
"enableCheckboxes": false,
}
<Cells {...props} />
You can even spread multiple props like this:
<Row {...row_props} {...selection_props} {...checkboxes_props} />
Combining discriminated unions and the spread operator
Now this is where it got a little tricky. We wanted to use both a discriminated union and the spread operator, while retaining code completion. Our first attempt didn't throw any errors, neither the ones it should have. (I have added the code as screenshots here so you can see the validation errors and lack thereof)
It took us some time to realize this was probably because we were using a variable instead of a constant, and TypeScript just couldn't figure out what magic we had been doing to it. We switched to a const for our second and third attempt:
Now for some reason TypeScript gives us an error on the <Row>
stating that a boolean is not assignable to true. It took us some time to figure out what was going on, but eventually my colleague Arnout found the solution! React was interpreting enableItemSelection
as a boolean instead of the fixed value true, because it might change during the lifetime of the selection_props
object. So technically we could just state that true cannot be changed
Not quite satisfied with this solution we realized that we could just define the type of the let variable that we started with, doh! This not only gives us errors on the <Row> when we do something wrong, but also when we assign the object to selection_props.
React
Diving into the React world was a really fun experience, I enjoyed all the great tutorials out there, and there are many flavors of doing things (functional vs class components, etc). I learned a lot about props vs state and how the relationship between parent/child components works.
Re-rendering
If you install the react-devtools plugin for chrome, you get some cool extra options in your console. One of the flags you can check is "Highlight updates", this will gives you some great insight into what's going on under the hood. The blinking borders show you which part of a component is re-rendering. This is a great way to double check your expectations of which features cause a re-render and prevent unneeded ones to improve performance.
A component will try to re-render after a setState
, even when no data has changed in that setState
. It will call the shouldComponentUpdate
method for itself and every child component. Every component that returned true will be re-rendered and a DOM comparison will be done to determine which updates need to take place.
Pure components
By default, the shouldComponentUpdate
function returns true, however class-based components can be configured differently. The recommended way of preventing a whole tree of components to re-render is a PureComponent.
A PureComponent does a shallow comparison on its props and state to determine if anything has changed. This means that any in-place modification of an object/array will return a false negative. Keep in mind to only use setState
with newly created objects/arrays. If nothing has changed the shouldComponentUpdate
method will return false. This should prevent a whole bunch of re-renders.
You can create a class based PureComponent by extending React.PureComponent
instead of React.Component
, a functional component can be wrapped in React.memo
.
Bookkeeping
In our own framework our re-usable components have an automated clean-up step that cancels the callbacks of any promises that where still on the go when the component got unloaded.
React components don't have this automated, so ensure that all the promises fired by a component are cleaned up in onComponentWIllUnmount
or by using a React Hook. If you don't prevent this React will throw a warning in the console, but even worse, your code might be looking for a DOMNode that no longer exists and throw an error. We're currently looking into ways to automatically clean up these promises, so keep an eye out for a future blog post if we manage to make something and can open source it.
The DOM diffing algorithm
Last but not least I'd like to share this excellent bit of explanation by the React team on how they decide when to move, update or create a DOM node: https://reactjs.org/docs/reconciliation.html#the-diffing-algorithm
If you manage to ensure that your keys are configured properly it should really help your application perform like crazy!