Dylan Vann

How to Incrementally Migrate 100k Lines of Code to Typescript

Using a Snapshot Test

Migrating a large project to TypeScript even with the loosest settings may be insurmountable. The number of errors is overwhelming.

You may think the best options for fixing this situation is to use a mix of TypeScript and JS (using the allowJs flag), however there is a risk of a large amount of code never being typed because there is not enough incentive and feedback to do so.

If you run TypeScript on a large project after renaming files you may be faced with something like:

Or there could be a lot more errors, this project started with 15k errors.

Unfortunately for a project of any sufficient size, you’re going to run into trouble trying to migrate to TypeScript in one go.

So what are your options?

Dimensions of ComparisonSolo Hackathon Over the weekendOrganize a team hackathonEnable Linter only on NEW and edited filesAdd to the checklist of development processes and coach the teamSnapshot Test of type errors
Fast to-do
Produces High quality resultsYMMV
Requires Low team-coordination
Non-breaking for WIP branches (and PRs)
Reliable on larger Repos or severly messy code
Can enable strictist rules on day 1
Easy to add stricter lint/checker rules
Will achieve 0 Warnings/Errors
Easily repeatable for new linters/checkers

Think about it.

Ideally you would like to achieve 0 errors and have an easily repeatable process for preventing new errors. To do this your best option may be creating a snapshot test.

On a high-level, using a snapshot test requires creating a test that runs TypeScript and saves a snapshot of all the errors along with filenames and line numbers. This test will fail anytime lines are shifted on a file with errors, or when new errors are added to the codebase — this serves as a reminder to fix type errors when adding or modifying code. This requires low coordination because it’s an automated approach.

It also becomes very easy to incrementally increase the strictness of type checking, the incremental approach is the same.

In essence, the snapshot test is closer to the code than any checklist process and it requires low team-coordination.

How to create a snapshot test of TypeScript errors?

This repo (DylanVann/typescript-migration-demo) shows a basic example of how to snapshot test TypeScript errors.

Here’s how it works, consider the following 3 untyped JS files:

// add.js
export default function add(a, b) {
  a + b
}
// subtract.js
export default function subtract(a, b) {
  a - b
}
// example.js
import add from './add'
import subtract from './subtract'

add('1', 3, 'hello world')

subtract('1', 3, 'hello world')

When we convert to TypeScript (changing file extensions and adding a tsconfig.json file) this will produce a number of type errors:

At this point you should run the snapshot test and commit the result. The snapshot of the errors will look something like this:

What happens when I fix or add type errors?

When you fix type errors you can run yarn check-ts -u to update the snapshot, and you will commit something like this:

If you were to add a type error by accident you would see something like this:

So at this point if you are doing PR reviews your reviewer would probably reject this change.

Using ESLint (or other tools)

This technique applies to any pattern that can be detected using code quality tools. For example it’s possible to write ESLint rules for bad practices specific to your codebase. You can then incrementally remove them using this technique.

Conclusion

Out of all the possible techniques to migrate to TypeScript this one has a lot of things going for it.

How does React Hooks affect testing?

How does React Hooks change traditional approaches to React app architecture and testing?

Not very much:

  • You should still write most of your components as stateless functional components (not using hooks).
  • You should still use a state management library for 90% of your application state.

Hooks should be reserved for special cases:

  • Animations.
  • Integrating non-react libraries.
  • Creating more complex reusable components (should be done sparingly).

Anytime you’d currently use a class component, you could instead use hooks. Also, if you haven’t already been avoiding class components you should avoid them.

Historically avoiding class components has been a good practice in React, and I think avoiding using Hooks for most components will continue to be a good approach.

Benefits of hooks over class components:

Most business logic should still live in your state management library, and it can be tested in isolation there instead of being tested in React components.

Reusable/stateful/complex components that require hooks should be tested using more integration like tests. The best library available for doing this right now is react-testing-library. The philosophy of react-testing-library is that your tests should resemble how your code is used, and it should test what your users care about. This works very well for testing hooks, because using this approach wether or not a component uses them is an implementation detail that should not affect how a test is written.

At some point there are going to be more state management libraries providing hooks APIs. For these I believe it would still be best to call them in container components, which could now be written as function components. These containers should be relatively simple, and could probably be untested, similar to most Redux containers.

In summary, if you want an app to be as maintainable as possible I think you should treat most hooks the way you treat class components now, avoiding using them in favor of relying on a state management library 90% of the time.

Ant Design Draggable Modal

The Modal from Ant Design, draggable.

GitHub

Trying something out with React Hooks.

React + SSR + NoScript + CSS in JS Fallbacks

Custom CSS for users with JS disabled.

What does that title even mean?

Assuming you have a site:

  • Using CSS in JS.
  • Using SSR (we’re specifically using emotion, which is very similar to styled-components).
  • That you’d like to work for users with JS disabled.

Then it means this is the article you need to read.

The specific site I was solving this issue for was created with Gatsby, which has all of the above setup by default.

The Scenario

You have some images which you would like to initially be hidden. They will then fade in when the user scrolls to them.

This can be implemented like this:

import React from 'react'
import VisibilitySensor from 'react-visibility-sensor'
import styled from '@emotion/styled'
import { css } from '@emotion/core'

const hiddenStyle = css`
  opacity: 0;
  transform: translate(0px, 60px) scale(1.05, 1.05);
`

const visibleStyle = css`
  opacity: 1;
  transform: translateX(0px) translateY(0px) translateZ(0px) scaleX(1) scaleY(1)
    scaleZ(1);
`

const Container = styled('div')`
  transition: width 0.7s ease 0s, opacity 1200ms, transform 1800ms;
  ${hiddenStyle};
  ${({ visible }) => visible && visibleStyle};
`

class EnterAnimation extends React.Component {
  state = {
    visible: false,
  }

  onChange = visible => {
    if (visible && !this.state.visible) {
      this.setState({ visible: true })
    }
  }

  render() {
    return (
      <VisibilitySensor partialVisibility onChange={this.onChange}>
        <Container visible={this.state.visible}>
          {this.props.children}
        </Container>
      </VisibilitySensor>
    )
  }
}

export default EnterAnimation

The EnterAnimation class wraps children and shows them with a css animation when it is scrolled into view (using react-visibility-sensor to detect this).

The problem with this code is that when Gatsby extracts our CSS it will extract our hidden styles as the default. This means users with JS disabled will not be able to see any elements wrapped in this component.

To get around this problem we can create specific styles for users with JS disabled.

1. Add a no-js class on <html>.

We’re using react-helmet to add attributes to our html. We can use the Helmet component to add a default no-js class to html. It’s important that this is only added during SSR or else it will break styles for users with JS enabled. Checking the typeof window allows us to determine if we’re doing SSR or not.

const IS_SSR = typeof window === 'undefined'

<Helmet>
    <html lang="en" className={IS_SSR ? 'no-js' : 'js'} />
</Helmet>
)

2. Remove no-js with a <script> in <head>.

We add a script in <head> to remove the no-js class (thanks to Paul Irish for this one-liner). Since it’s a script it will only be removed for users who don’t have JS disabled, and it won’t run during SSR.

<Helmet
  title={`${title} | ${siteTitle}`}
  meta={[{ name: 'description', content: siteDescription }]}
  script={[
    {
      type: 'text/javascript',
      innerHTML:
        "document.documentElement.className = document.documentElement.className.replace(/\\bno-js\\b/,'js');",
    },
  ]}
>
  <html className="no-js" />
</Helmet>

3. Add a no-js specific style.

To add a no-js specific style we’ll use emotion.

const Container = styled('div')`
  transition: width 0.7s ease 0s, opacity 1200ms, transform 1800ms;
  ${hiddenStyle};
  html.no-js & {
    ${visibleStyle};
  }
  ${({ visible }) => visible && visibleStyle};
`

We target the <html> element when it has the no-js class, then use & to target our specific component. This style will be included in SSR, but will only be enabled when the no-js class is present. For our JS users the class will be removed before the first render.

That’s about all there is to it. Users without JS don’t make up a large part of most audiences, but when the tooling makes it easy to support them why not make an attempt?

Cleaner Markup with React 16

Cleaner Markup with React 16

One of the new features in React 16 is that it allows you to return an array of components to render.

What is this useful for? Mostly for rendering cleaner markup. This will lead to a nicer looking DOM (for anyone looking), as well as slightly lighter payloads for SSR. It may also be useful in certain scenarios where the exact structure of the DOM is important.

Recently I’ve been working on creating accessible forms code. I’m aiming to make the components maximally composable, and aiming to keep the code as DRY as possible.

In trying to do this I’ve started using higher order components for adding errors, labels, and legends to the various form components. It ends up looking like this when writing a particular input that needs a label and inline error.

// TextArea.js
import React from 'react'
import compose from './compose'
import withLabel from './withLabel'
import withError from './withError'

const TextArea = props => <textarea className="input" {...props} />

TextArea.displayName = 'Text'

export default compose(
  withLabel,
  withError
)(TextArea)

The withLabel component looks like:

// withLabel.js
import React from 'react'

export default Base => {
  const WithLabel = props => (
    <div>
      {!!props.label && (
        <label key="label" className="label" htmlFor={props.id}>
          {props.label}
        </label>
      )}
      <Base key="base" {...props} />
    </div>
  )
  WithLabel.displayName = `withLabel(${Base.displayName})`
  return WithLabel
}

It associates the label with the base component using the id prop and htmlFor.

The error component looks like:

// withError.js
import React from 'react'

const errorId = id => `${id}-error`

export default Base => {
  const WithError = props => (
    <div>
      <Base
        aria-describedby={!!props.error ? errorId(props.id) : null}
        aria-invalid={!!props.error}
        {...props}
      />
      {!!props.error && (
        <span className="error" id={errorId(props.id)}>
          {props.error}
        </span>
      )}
    </div>
  )
  WithError.displayName = `withError(${Base.displayName})`
  return WithError
}

It associates the inline error with the base component by giving the base component an aria-describedby prop referring to the errors’ id. It also adds an aria-invalid prop if an error is present.

The markup rendered by the Text component with a label and error ends up looking like:

<div class="error-container">
  <div class="label-container">
    <label class="label" for="invalid-textarea-before-id">Invalid Text</label>
    <textarea
      aria-describedby="invalid-textarea-before-id-error"
      aria-invalid="true"
      class="input"
      id="invalid-textarea-before-id"
    ></textarea>
  </div>
  <span class="error" id="invalid-textarea-before-id-error">Text Error</span>
</div>

Not very pretty. We’ve got a bunch of extra divs, and we have a bunch of nesting we don’t need.

In React 16 we can make our withLabel and withError components return arrays or fragments. We will use the new JSX fragment syntax <></>.

Our HOC will now look like:

// withLabel.js
import React from 'react'

export default Base => {
  const WithLabel = props => (
    <>
      {!!props.label && (
        <label className="label" htmlFor={props.id}>
          {props.label}
        </label>
      )}
      <Base {...props} />
    </>
  )
  WithLabel.displayName = `withLabel(${Base.displayName})`
  return WithLabel
}

After changing over withError the rendered markup will look like:

<label class="label" for="invalid-textarea-id">Invalid Text</label>
<textarea
  class="input"
  aria-describedby="invalid-textarea-id-error"
  aria-invalid="true"
  id="invalid-textarea-id"
  label="Invalid Text"
  error="Text Error"
></textarea>
<span class="error" id="invalid-textarea-id-error">Text Error</span>

No wrapping divs, just exactly what we wanted rendered.

You can find the source for this at DylanVann/react-cleaner-markup-example.

1 of 4
© 2018 Dylan Vann