10 Things I Have Learned Open Sourcing A React Hook Without Going Crazy

10 Things I Have Learned Open Sourcing A React Hook Without Going Crazy

·

0 min read

This is my 10 gotchas how I spend tons of time open sourcing 100 LOC. And my attitude to swap frustration with just enough motivation to become 1% better. All of that while sharing some value with the world (via this post and an open source package).

So.

I had an idea to add a MailChimp subscription form to my blog via hooks. I thought it would be nice to isolate it into an open source package. 60 LOC for a hook and another 40 LOC for sloppy tests took surprisingly big amount of time.

This post is part of my personal journey you can join and learn for free from my mistakes.

Intention

  • Resisting perfecting of every step to increase practicality and allowing myself to move forward, faster.
  • Overcome rising complexities with reasonable amount of frustration.
  • Document my discoveries.

The result

This is an usage example of react-use-mailchimp hook to embed a MailChimp form into a React app:

export const Form = () => {
  const url = 'URL_YOU_CAN_OBRAIN_FROM_MAILCHIMP_UI'
  const [{ loading, error, data }, subscribe, reset] = useMailchimp({ url })
  const [email, setEmail] = useState('')

  return (
    <form
      onSubmit={e => {
        e.preventDefault()
        subscribe({ EMAIL: email })
      }}
    >
      <input onChange={e => setEmail(e.target.value)} onFocus={reset} />
      <button type={'submit'}>Submit</button>
      <div>
        {!!loading
          ? 'Loading...'
          : error
          ? 'Error during subscription'
          : data && data.result === 'success'
          ? 'Subscribed!'
          : null}
      </div>
    </form>
  )
}

My gotcha's

Here is a list of my «gotchas» and takeaways during development.

#1. Configuring Jest

From the beginning I've decided that I will have some tests, at least medium quality ones. Without thinking too hard I've checkout out open source code to see how people do their tests. What I found is a config that works for me:

jest.config.js

module.exports = {
  testEnvironment: 'jsdom',
  transform: {
    '^.+\\.jsx$': 'babel-jest',
    '^.+\\.js$': 'babel-jest',
  },
  setupFiles: ['<rootDir>/jest.init.js'],
}

jest.init.js

import '@babel/polyfill'

This quickly allowed me to skip the docs at least for some time and move on to get stuff done.

#2. Testing with react-hooks-testing-library

First I've installed react-testing-library. But soon discovered another option to test react hooks — react-hooks-testing-library.

Usage example:

import { renderHook, act } from 'react-hooks-testing-library'
import useCounter from './useCounter'

test('should increment counter', () => {
  const { result } = renderHook(() => useCounter())
  act(() => result.current.increment())
  expect(result.current.count).toBe(1)
})

No additional components for wrapping up hooks manually. Neat!

Other big deal about react-hook-testing-library is that it allows to handle asynchronous nature in your react hook. With a small caveat. More on that later.

This was quite... annoying one. npm link command can be used to test your package in local development without publishing it to npm registry. Sweet, convenient, did not work out of the box for me.

React was throwing error about having two React instances in a same application. The reason was is some woodoo magic in npm linking.

The solution was simple, ugly and necessary.

This problem can also come up when you use npm link or an equivalent. In that case, your bundler might “see” two Reacts — one in application folder and one in your library folder.

Assuming myapp and mylib are sibling folders, one possible fix is to run npm link ../myapp/node_modules/react from mylib. This should make the library use the application’s React copy.

I assume it would be resolved in future versions of npm / react.

#4. «Better npm publish»

«Better npm publish». This title stuck with me some time ago. I have never check it out but doing a quick google search revealed a tool called np to automate package publishing process.

package.json

{
  "scripts": {
    "publish": "np"
  }
}

Using this tool adds some amount of safety without adding much complexity. Sweet!

#5. Fighting myself annoying bug

In order to be honest I need to say that this bug was a significant part of pain while writing 100 LOC. Just because of a silly bug that was successfully hiding from my attention. For an hour, or two, or...

Here is a line of code with a bug:

jsonp(url, opts, callback)

Yeap, that simple line of code. But url was a real URL but not the one that I need. Naming is important, and so is sleeping enough.

#6. Fighting async nature of a react hook

If there is some async stuff happening in your hooks you may wonder how to test. There is a simple way.

Here is a part of test:

act(() =>
  /* this one makes a http request */
  result.current[1]({
    EMAIL: EMAIL,
    NAME: '',
  })
)
/* checks loading before request */
expect(result.current[0].loading).toBe(true)
/*
        sweet part.
        this one «waits» until there state of a hook will change.
    */
await act(async () => {
  await waitForNextUpdate()
})
/* checks loading after request */
expect(result.current[0].loading).toBe(false)

But in order to follow that way I had to spend two hours realizing that I need to use alpha version of React.

package.json

{
  "peerDependencies": {
    "react": "^16.8.6"
  },
  "devDependencies": {
    "react": "16.9.0-alpha.0",
    "react-dom": "16.9.0-alpha.0",
    "react-test-renderer": "16.9.0-alpha.0"
  }
}

During development in order tests to work you need apha version of react. But in order to use it you can leave ^16.8.6 as a dependency.

#7 Let's steal an API from react-apollo

At first my state for holding data looked like this:

const [{ status, message }, subscribe] = useMailchimp({ url })

Then I remembered that react had a nice API to work with requests. And what they come to was something like:

const = () => (
  <Query query={GET_DOGS}>
    {({ loading, error, data }) => {
        /* ... */
    }}
  </Query>
)

I though it was better. API of my hook would be similar to something in the wild. And also I would not expose string variables.

So I've converted an API into:

const [{ loading, error, data }, subscribe] = useMailchimp({ url })

Bonus: data holds an original JSON representation of an API response from MailChimp.

#8. I need a reset() action

I need to decide what API my hook exposes. Using this hook by myself I realized that I do need a reset functionality for hook.

Done!

const [state, subsctibe, reset] = useMailchimp({ url })

#9. Zero config, many builds

Digging into open source libs I've stumbled upon microbundle.

The zero-configuration bundler for tiny modules, powered by Rollup.

package.json

{
  "scripts": {
    "build": "microbundle -o dist/ --sourcemap false --compress false"
  }
}

Oh, that nice feeling then zero config means minimal effort from your behalf!

#10. Exposing your work teaches you

Final lesson.

Although tasks seems to look quite easy it steal manages to eat surprising amount of time. In that case I am trying to remember that it is partially because of me and partially because of the freaking complexity of reality. :) This mindset leaves just enough pressure on me to improve but does not make me overwhelmed or frustrated too much.

As you can see you can learn a bunch of stuff by doing an open source job. Also you can skip learning some stuff which is good to keep personal momentum and make job done.

Open Source

All of this is packed into react-use-mailchimp package we can enjoy in any of our react application.

If there was any value and you want to get some more — go check out my blog. The good stuff is waiting for you!