Thursday, December 3, 2020
5 React Fundamentals I Recently Picked Up

Recently I've gone through Kent C. Dodds' "The Beginner's Guide to React" and even though I've been working with React for a while, I learned a couple of new things.

Proptypes Are Simple Functions

Since I started using React, I've used the prop-types package to verify props. I never actually thought about how this worked, but it turns out that a prop-type is a simple function that returns an Error if the input is invalid.

So you could write your own "prop-type-checker":

function PropChecker(props, propName, componentName) {
  // ...
}

MyComponent.propTypes = {
  myProp: PropChecker,
}

You'll probably never need this in day-to-day application development, but you could use this if you're writing a React library and you want to give your users a great developer experience by validating the props to prevent weird behavior.

Successive ReactDOM.render Calls Will Re-render Your Tree

I used to think that ReactDOM.render would unmount everything below the target element and mount a completely new React tree. But in reality, if you previously rendered to that node, it will just re-render the tree, just like when you trigger a re-render through React state updates.

I'd still say that calling ReactDOM.render multiple times is probably not good, but it's definitely not as bad as I thought!

Using Native Forms Gives You Lots of Goodies

I've gotten into the bad habit of leaving out the <form> tag and using the onSubmit method for my forms. Most of the time I just threw together a couple of <input /> elements and a <button type="button"> to submit the form via onClick.

Not only does this suck for accessibility reasons, but it also misses out on the UX patterns that the browser otherwise gives you for free, like submitting a form by pressing enter in one of its fields.

You can also leave these forms uncontrolled and just rely on the onSubmit-callback of the <form> element, which is more performant than updating the state on every keystroke (though the difference might not be noticable).

Colocate State

In my day job, we use Redux and so I've seen many cases where this should be taken more seriously.

Sometimes we need state to span across many children so we lift it up or even extract it into Context or another state management solution like Redux. But we rarely pay attention to where we could colocate the state to simplify our code.

So maybe you should also start looking for places in your source code where you can push state further to where it's needed.

Use Status Fields More Often

I've made HTTP requests in many different ways. I've had an error state and checked that for null, like this:

export default function App() {
  const [data, setData] = useState()
  const [error, setError] = useState()

  function handleSubmit(event) {
    event.preventDefault()
    const username = event.target.elements.username.value

    fetch(`https://api.github.com/users/${username}/repos`)
      .then((response) => response.json())
      .then((responseData) => setData(responseData))
      .catch((e) => setError(e))
  }

  let content

  if (error) {
    content = <p>Oops, something has gone wrong...</p>
  }

  if (data) {
    content = <pre>{JSON.stringify(data, null, 2)}</pre>
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input placeholder="GitHub username" name="username" />
        <button type="submit">Load Repositories</button>
      </form>
      <div>{content ?? "Go ahead and fetch some repositories!"}</div>
    </>
  )
}

But now if a query fails and we set the error state, it will stay in that state forever, because we forgot to clear it. Also, the code is quite dependent on the order of the if-statements and their return statements. If we placed the if (data) statement above the error statement, we would have the same problem, but the errors for subsequent requests would be ignored.

The most error-tolerant way to code this is to use a status state field:

export default function App() {
  const [data, setData] = useState()
  const [status, setStatus] = useState("idle")

  function handleSubmit(event) {
    event.preventDefault()
    const username = event.target.elements.username.value

    setStatus("pending")
    fetch(`https://api.github.com/users/${username}/repos`)
      .then((response) => response.json())
      .then((responseData) => {
        setStatus("resolved")
        setData(responseData)
      })
      .catch((e) => {
        // logErrorToService(e);
        setStatus("rejected")
      })
  }

  let content

  if (status === "idle") {
    content = <p>Go ahead and fetch some user repos!</p>
  }

  if (status === "pending") {
    content = <p>Loading...</p>
  }

  if (status === "rejected") {
    content = <p>Oops, something has gone wrong...</p>
  }

  if (status === "resolved") {
    content = <pre>{JSON.stringify(data, null, 2)}</pre>
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input placeholder="GitHub username" name="username" />
        <button type="submit">Load Repositories</button>
      </form>
      <div>{content}</div>
    </>
  )
}

Your request can only ever be in one state. Your logic should resemble that.

This way you can see which component is rendered in which state, and that's what React is all about.

Also, you no longer have to rely on the order of if statements can and easily distinguish idle and loading states.

Check out the CodeSandbox here:


So those are some of the things I picked up in "The Beginner's Guide to React". I've been working with React for over a year now, but there's always something new to learn, maybe even some of the basic stuff!

I recommend you check out "The Beginner's Guide to React" for yourself and try not to skip too much, the best learnings are the patterns we see along the way.

Cheers!