Above, Below, and Beyond Tech Talk

by Rahel Lüthy

November 21, 2018

How we use React/Redux with TypeScript

Our team develops visual applications in the field of medical informatics. We started using React/Redux with TypeScript a few months ago. This post is a highly opinionated summary of best practices that evolved over time:

A bit of personal background, to give certain decisions more context: I have been developing user interfaces for over 20 years. Java and OOP have been loyal companions throughout most of this time. A few years ago, I started doing more and more FP, mostly in Scala & Elm, but also in Java (hi vavr 👋).

Our team members come from all sorts of backgrounds. When deciding on a web app stack, opinions varied a lot. We finally settled on React/Redux + TypeScript as a compromise – it turned out to be a good decision.

Favor Types Over Classes

Coming from an OOP background, it was comforting that TypeScript brings familiar OOP constructs to the table. However, TypeScript is based on a structural type system, which can confuse Java developers very quickly:

class Patient {

    firstName: string
    lastName: string

    constructor(firstName: string, lastName: string) {
        this.firstName = firstName
        this.lastName = lastName
    }
}

const patient: Patient = { firstName: 'Ada', lastName: 'Lovelace' }

console.log(patient instanceof Patient) // false – seriously?! 🤔

I am sure that one could get used to the TypeScript way of working with classes. However, we somehow ended up not using classes at all 🤷. Instead, we exclusively model our data with read-only types:

type Patient = Readonly<{
    id: PatientId
    caseId: CaseId
    bed: BedId
    firstName: string
    lastName: string
}>

Super concise and very safe to use.

Don’t Fear Stringly Types

Any experienced Java programmer avoids stringly types like the plague. In Java, this makes sense, they prevent the compiler from helping us find errors. The TypeScript compiler works differently, so be ready to embrace patterns which you would avoid in Java:

type Patient = Readonly<{
    gender: 'male' | 'female' | 'non-binary'
}>

Code completion works perfectly fine here:

And the compiler detects errors flawlessly:

const smartStringHandling = (patient: Patient) => {
    if (patient.gender === 'whatever') { // compile error
        
    }
}

To give a more advanced example, here’s how we base our action types, actions, and reducers on simple strings:

actionTypes.ts

const ADD_MESSAGE = 'message/add'
const ADD_TODO = 'todo/add'

actions.ts

type Action = AddMessageAction | AddTodoAction

type AddMessageAction = Readonly<{
    type: typeof ADD_MESSAGE
    message: Message
}>

type AddTodoAction = Readonly<{
    type: typeof ADD_TODO
    todo: Todo
}>

reducer.ts

const reducer = (state: State, action: Action): State => {
    switch (action.type) {
        case ADD_MESSAGE:

            // 🎉 this would require a cast in Java
            const message = action.message

            return {
                ...state,
                messages: state.messages.push(message)
            }
        case ADD_TODO:
            return {
                ...state,
                todos: state.todos.push(action.todo)
            }
    }
}

Favor Composition Over Inheritance

As mentioned above, we don’t really use TypeScript’s OOP features, so using inheritance has never been very tempting. Instead, we often use a mix of composition, union types, and intersection types to foster code re-use:

type Patient = Readonly<{
    address: Address // Composition
    gender: 'male' | 'female' | 'non-binary' // Union Type
}>

type Displayable = Readonly<{
    displayName: string
}>

type DisplayablePatient = Patient & Displayable // Intersection Type

Immutability FTW

We haven’t seen many runtime errors, but the ones that occurred were all caused by inconsistent mutations. That’s why we settled on using the immutable collections library to make all our state completely read-only:

import { Map } from 'immutable'

type State = Readonly<{
    bedByPatient: Map<PatientId, BedId>
}>

Nominal Typing

Using type aliases for your identifiers seems very convenient. No need for extra wrapping, and very readable code:

type PatientId = string
type BedId = string

type State = Readonly<{
    bedByPatient: Map<PatientId, BedId>
}>

Unfortunately, things look more type-safe than they are. Aliases are nothing more than what their name implies: they are simple synonyms. Any string can be used in place of a PatientId or BedId – and vice versa:


const state: State = {
    bedByPatient: Map<PatientId, BedId>()
}

// Compiles just fine, which is NOT what we want
state.bedByPatient.set('foo', 'bar')

We want to have types which can be distinguished by the compiler because they have different names, even though they share the same structure (a string). This is known as “nominal typing”. The TypeScript Deep Dive Book gives a good list of nominal typing patterns.

We are using the enum-based brand pattern to get the desired compile-time safety:

enum PatientIdBrand {}
type PatientId = PatientIdBrand & string

enum BedIdBrand {}
type BedId = BedIdBrand & string

type State = Readonly<{
    bedByPatient: Map<PatientId, BedId>
}>

const state: State = {
    bedByPatient: Map()
}

// Compile error:
// Argument type '"foo"' is not assignable to parameter of type 'PatientId'
state.bedByPatient.set('foo', 'bar')

Explicit Types Enhance Readability

While it is often possible to not specify types explicitly, they still sometimes enhance code readability (and IDE completion, for that matter). Container components are a good example: they involve a lot of “plumbing”, where input/output types have to match, so explicit types are a plus here. This is how our container components tend to look:

type Props = Readonly<{
    patientId: PatientId
}>

type FromStateProps = Readonly<{
    patient: Patient
}>

const mapStateToProps = (state: State, props: Props): FromStateProps => {
    const patient = getPatient(state, props.patientId)
    return {
        patient
    }
}

type FromDispatchProps = Readonly<{
    onMouseEnter: () => void
    onMouseOut: () => void
}>

const mapDispatchToProps = (dispatch: Dispatch<PatientsAction>, props: Props): FromDispatchProps => {
    return {
        onMouseEnter: () => dispatch(selectPatients(ImmutableList.of(props.patientId))),
        onMouseOut: () => dispatch(selectPatients(ImmutableList()))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(ExampleComponent)

TSS: CSS + TypeScript = 😍

As a developer, CSS has always been the scary corner of my applications. It uses a global namespace, you cannot use variables, and it’s almost impossible to tell which code is even in use at all. So you end up treating your CSS very differently from the rest of your code: no refactorings, no re-use, no clean-up.

We are using Material-UI in all our projects, so it did not take much convincing to also use their styling solution. It uses JSS at its core and has excellent TypeScript support. This is how a basic component looks:

import { createStyles, withStyles, WithStyles } from '@material-ui/core'
import * as React from 'react'

const styles = createStyles({
    root: {
        backgroundColor: 'steelblue'
    }
})

type Props = Readonly<{
    text: string
}> & WithStyles<typeof styles>

const ExampleComponent = ({ text, classes }: Props) =>
    <div className={classes.root}>{text}</div>

export default withStyles(styles)(ExampleComponent)

Look Ma, No Semicolons!

And finally, a good practice that is not specific to React nor TypeScript: make your code prettier! We use husky to kick off code formatting before each git commit. Here’s our current configuration:

.prettierrc:

{
  "tabWidth": 4,
  "useTabs": false,
  "semi": false,
  "singleQuote": true,
  "printWidth": 120
}

Thanks for reviewing this post, Ben!


May 24, 2018

React SVG Tooltips

SVG has built-in support for tooltips through its <title> element. However, the rendered text looks very basic and cannot easily by styled:

Tooltips are an important design element when creating information-heavy, visual applications. They allow keeping the user interface clean, providing information only when needed. As Shneiderman (1996) put it in his famous visual information-seeking mantra:

Overview first, zoom and filter, then details-on-demand
Overview first, zoom and filter, then details-on-demand
Overview first, zoom and filter, then details-on-demand

Tooltips cover the details-on-demand aspect. However, in order to provide such details in an appealing way, built-in SVG tooltips are often not sufficient.

SVG Tooltips

There’s actually a pretty good technology out there to design appealing graphical UI elements: SVG! It is way superior to simple text boxes.

In an ideal SVG world, tooltips would:

  1. Be specific to an element
  2. Be visible on demand (mouse hovering)
  3. Support fully customizable SVG contents
  4. Appear always on top

Good News: If you’re using React, I have got you covered! Meet react-svg-tooltip, my npm package that addresses all SVG tooltip needs.

Intro to react-svg-tooltip

The library offers a Tooltip component which can be embedded in any SVG element hierarchy:

import * as React from 'react';
import { Tooltip } from 'react-svg-tooltip';

const App = () => {

    const circleRef = React.createRef<SVGCircleElement>();

    return (
        <div className='App'>
            <svg viewBox='0 0 100 100'>
                <circle ref={circleRef} cx={50} cy={50} r={10} fill='steelblue'/>
                <Tooltip for={circleRef}>
                    <rect x={2} y={2} width={10} height={5} rx={.5} ry={.5} fill='black'/>
                    <text x={5} y={5} fontSize={2} fill='white'>Yay!</text>
                </Tooltip>
            </svg>
        </div>
    );
};

export default App;

Edit pk7p4y9v3q

The component covers all requirements listed above:

  1. Its for property accepts a reference to an element which serves as the mouse trigger
  2. All mouse listener handling is happening behind the scenes
  3. Arbitrary SVG can be used as tooltip contents. In fact, the library itself does not provide an actual tooltip. Instead, it gives you a 0-based coordinate system, so you can place your favorite SVG elements in whichever style suits your needs.
  4. Contents are attached to the root svg element (using a React portal behind the scenes). Thereby your tooltip will always be rendered last, i.e. always on top.

As usual, all code is on GitHub, feedback is very welcome, and PRs are highly appreciated!


May 3, 2018

GitHub Pages Custom Domains Via HTTPS

Two days ago, GitHub announced the support of HTTPS for custom domains. In addition to this blog, I maintain a few websites for friends and family, all configured through A records – time for migration!

Officially, the migration is a simple matter of toggling the “Enforce HTTPS” button, but the button was disabled for all of my sites. The explanation wasn’t particularly encouraging: Unavailable for your site because your domain is not properly configured to support HTTPS.

Well, time for RTFM (not my favorite hobby).

These are the steps that finally worked for me:

HTTPS Migration Steps

Step 0: Mixed Content Prevention

First things first: Prepare your site’s content. Ensure that all your assets (links to CSS, JS, etc.) are loaded via HTTPS. Otherwise browsers might block your “Mixed Content”.

Step 1: DNS A-Record Migration

On your provider’s website, change all A records to point to the following four new IPs:
 

192.30.252.153

192.30.252.154

185.199.108.153

185.199.109.153

185.199.110.153

185.199.111.153
 

This change might take a few hours to become effective (depending on the TTL). Use dig to check whether the new DNS configuration is ready:

dig netzwerg.ch +noall +answer

It should look like this:

; <<>> DiG 9.10.6 <<>> netzwerg.ch +noall +answer
;; global options: +cmd
netzwerg.ch.		10800	IN	A	185.199.110.153
netzwerg.ch.		10800	IN	A	185.199.111.153
netzwerg.ch.		10800	IN	A	185.199.108.153
netzwerg.ch.		10800	IN	A	185.199.109.153

Step 2: Trigger Certificate Generation

In order for GitHub to detect that your site is now ready for migration, you apparently need to trigger a detection script. Any change to your repository’s CNAME file will do, so e.g. make a minor change and revert it, or make a harmless whitespace edit. As a result, GitHub will now generate the certificates necessary for HTTPS.

Step 3: Wait For Certificate Availability

Now the “Enforce HTTPS” button will still be disabled, but the explanation will be much more encouraging: Not yet available for your site because the certificate has not finished being issued. More patience required, we’re getting there…

Step 4: Enforce HTTPS

It might take a few hours, but as soon as this message is gone, you can access your site via HTTPS. Check that everything still works (particularly your new HTTPS asset links). Once you’re happy, toggle the “Enforce HTTPS” switch – Voilà!


Older Posts » Archive