Zach Bloomquist

Moving From React to Preact: A Developer’s Story

For the past few months, I’ve been working on a single-page application built using Facebook’s React framework.

Those who know me know that I’m obsessed with performance. Getting me on the React bandwagon took years simply because I didn’t like the bloat it introduces. Even though I’ve grown to love React for the deftness it affords when designing front-ends, I still have an issue with all the bloat it introduces.

Recently, I heard about the Preact project — a drop-in, API-compatible replacement for the React framework that’s only 3kb in size compared to React’s 135kb. It’s faster and easier to understand than React. When I heard about this framework, I knew I had to try it. I’ve documented my efforts here (including some troubleshooting tips) to guide others who would follow this path.


The React App

Before transitioning to Preact, I wanted to examine my existing app so I could be aware of any potential pain points I’d encounter while transitioning.

My application was created with create-react-app. This means a lot of the complexity of the build environment is hidden away in packages like react-scripts. This is good for a new user, but potentially bad when trying to switch away from React.

I make heavy use of React’s createContext API to enable authentication and back-end use throughout the app. Preact only supports the legacy context API, so I needed to use Georgios Valotasios’s preact-context library to provide those APIs.

My app uses react-router for routing and passing state between linked components. I had some concerns about this working.


s/React/Preact/g

Preact has a helpful guide for switching. They provide a shim, preact-compat, that allows you to swap your codebase over with minimal code changes. There’s also a longer guide to migrating your codebase to use preact without the compat libraries.

I chose to do the full migration in order to take advantage of the full performance benefits — the preact-compat library adds 2kb to Preact’s 3kb size. I wouldn’t have blinked at 2kb before, but now the bloat of React is no longer in the way, 2kb seems like a whole lot more.

First, install the preact package and get rid of react:

yarn add preact
yarn remove react react-dom

This will make all of your React plugins begin to throw warnings about unmet peer dependencies, this is fine. Currently there is no way to tell these plugins about Preact.

Then, use the following command to replace all the import X from 'react' statements in my package with import X from 'preact' (assumes all your code is in src):

find ./src/ -type f -print0 | xargs -0 sed -Ei "s/(['\"])react(-dom)?(['\"])/'preact'/g"

The import in index.js must be corrected manually. Replace the React and ReactDOM imports with

import React, { render } from 'preact';

Preact moves the render method out of ReactDOM and into its own export, so change the ReactDOM.render call to just a call to render .

To fix React Contexts, install preact-context with yarn add preact-context. Then, add the import statement to all your Contexts:

import { createContext } from 'preact-context';

After changing React.createContext calls to createContext calls, all that’s left to do is run yarn start and pray.


You’re done!

(In a perfect world,) your app should now be working with Preact instead of React. Kudos to the developers of Preact for creating a drop-in replacement for the temperamental React framework. Enjoy your faster load times, easier-to-debug code base, and lower bandwidth usage!

If you did encounter issues, continue on to Troubleshooting, where I’ve outlined a few of the issues I ran into when doing this on my app.


Troubleshooting

Of course, my migration did not go flawlessly — things never seem to, do they? I hope yours does, but if it does not, here are the issues I encountered and how I overcame them.

Context changes

My first issue was that changes being broadcast from a Context were no longer being sent to child components. I had the Context set up like this:

<Provider value={this.state}> ... </Provider>

Context Consumers are supposed to re-render whenever the Provider’s value changes. In React, the above line would cause all dependent components to re-render when the Provider’s state changed.

In Preact, this stopped working. I’m assuming this is because Preact does not copy state on setState; instead, it modifies state in place. Because JS object comparisons are done by reference, no change appears to happen and so the Consumer components are never re-rendered.

To get around this, I’m copying state to a new object whenever it changes:

<Provider value={Object.assign({}, this.state)}> ... </Provider>

If anyone knows a cleaner way, please let me know.

React Developer Tools

I discovered that my React developer tools were no longer working. This is because Preact does not include the developer tools in the main bundle to cut down on unnecessary load time.

To fix this, I just added the following line to index.js to load the debug module if this module is being hot-reloaded by Webpack:

if(module.hot) require('preact/debug')

React Router errors

The last issue I encountered was a problem with react-router-dom. The BrowserRouter component was throwing the following error during render:

Warning: Failed prop type: Invalid prop `children` supplied to `Router`, expected a ReactNode.

For some reason, the <div> element that was the immediate child of my BrowserRouter was not being recognized as a valid component. I tracked down this error to the definition of isValidElement in prop-types:

var REACT_ELEMENT_TYPE = (typeof Symbol === 'function' &&
  Symbol.for &&
  Symbol.for('react.element')) ||
  0xeac7;
var isValidElement = function(object) {
  return typeof object === 'object' &&
  object !== null &&
  object.$typeof === REACT_ELEMENT_TYPE;
};

The <div> fails the isValidElement test because it has no $$typeof property. The only thing I can seem to figure out about $$typeof is that it’s a special property React uses in order to distinguish React-created DOM objects from native DOM objects.

I tried adding a hook to preact to automatically populate the $$typeof on new VNode (virtual nodes) in the DOM, but that just uncovered another host of issues related to differences between the React virtual DOM and Preact’s VDOM.

So, with a heavy heart (it’s 2 kilobytes of code!!), I decided to install preact-compat (2kb!!!) and patch it into my app.

Doing this was fairly simple, just yarn add preact-compat and add 2 dependencies to your resolve.alias configuration in your webpack config:

alias: {
  'react': 'react-compat',
  'react-dom': 'react-compat'
},

Note: Because I created my app with create-react-app, I had to run yarn eject so that I could edit my webpack config instead of using one from node_modules. You may or may not have to do this first.

After doing this, react-router-dom worked like a charm.

I also went through my project and replaced all my preact imports with preact-compat just to match the documented way of doing it:

find ./src/ -type f -print0 | xargs -0 sed -Ei "s/'preact'/'preact-compat'/g"