← Back to the Note Garden

Beginner's Guide to React

22 min read

JavaScript

React

Egghead

Kent_Dodds

Jump to a Section:

Code files for the course are all here: https://github.com/kentcdodds/beginners-guide-to-react.git

Create a user interface with createElement API

To write React, we need to have the React library and the React-DOM library (to render items on the HTML page).

We can use the createElement function similar to document.getElementById - we provide the type of element we want to make, and an object with props to give it. Then we use the ReactDOM.render to pass in the element we want to render, and the DOM item to append it to.

Children for elements can be done two ways - either as a prop passed in through the options, or as a third argument to the createElement call.

  const rootElement = document.getElementById('root')
  const element = React.createElement('div', {
    className: 'container',
    children: 'Hello World',
  })
  ReactDOM.render(element, rootElement)

  // can also do children this way:
  const element = React.createElement('div', {
    className: 'container'
  }, 'Hello World')

If you don't have props to pass, instead of the options object we can pass null.

Create user interface with JSX syntax

JSX is an HTML-like syntax that React uses. To do this, we need to make sure to include Babel (or something similar that compiles our scripts) so the browser knows how to handle our code.

With this, we can re-write the previous code like this:

  const rootElement = document.getElementById('root')
  const element = <div className="container">Hello World</div>
  ReactDOM.render(element, rootElement)

Use JSX effectively

To pass in variables or JS expressions, we can put that in between {}.

const children = "Hello World"
const element = <div className="container">{children}</div>

This will cause the children to be appended as the third argument to createElement, once it's compiled. If we wanted this passed in as a prop instead, we can do that as well:

const element = <div className={className} children={children} />

With React, we can also make items self-closing even if they aren't in HTML. Since the div above doesn't have any text written inside it, we can now make it self-closing.

We can also pass in an object of all the props, as well as directly add additional props. If we need to overwrite some of the provided props, we can do that too by adding them after the provided props.

const children = 'Hello World'
const className = 'container'
const props = {children, className}
const element = <div id="app-root" {...props} className="not-container" />

Render two elements side by side with fragments

ReactDOM.render only takes two arguments - the item to render, and what it attaches to. So if we wanted to show two span elements, it can't do that. That's why React.Fragment exists - it lets us pass in multiple arguments and then render a single item, the fragment.

// Doing this with the createElement API looks like this:
const helloElement = React.createElement('span', null, 'Hello')
const worldElement = React.createElement('span', null, 'World')
const element = React.createElement(
  React.Fragment,
  null,
  helloElement,
  worldElement
)
ReactDOM.render(element, document.getElementById('root'))

// Or we can do this using JSX! These two examples end up exactly the same - one is a shortcut for the other:
const element1 = (
  <React.Fragment>
  <span>Hello</span> <span>World</span>
  </React.Fragment>
)

const element2 = (
  <>
  <span>Hello</span> <span>World</span>
  </>
)

Create reusable React component

If we want to make reusable components, we can do that by writing a function with a capital letter name that accepts props. Then when we use it, React knows to call createElement using that function and it can then render those components.

const Message = props => <div className="message">{props.children}</div>
const element = (
  <div className="container">
    <Message>Hello World</Message>
  </div>
)

// Custom components can also be nested, and/or contain other tags
<Message>
  Hello World
  <Message>Goodbye World</Message>
  <span>Hey there</span>
</Message>

// you can also pass named props to your function instead of using children
const Message = props => <div className="message">{props.msg}</div>
const element = (
  <Message msg="Hello World" />
)

Validate custom React component props with propTypes

React has a way to validate props in custom components on runtime, so you can make sure you're getting props you expect and they're used properly.

function SayHello({firstName, lastName}) {
  return (
    <div>
      Hello {firstName} {lastName}
    </div>
  )
}

// we can put this directly on the function (SayHello.propTypes), but it's common to factor this out on it's own then pass it
const PropTypes = {
  string(props, propName, componentName) {
    if (typeof props[propName] !== 'string') {
      return new Error(
        `Hey, the component ${componentName} needs the prop ${propName} to be a string, but you passed a ${typeof props[propName]}`
      )
    }
  }
}

SayHello.propTypes = {
  firstName: PropTypes.string,
  lastName: PropTypes.string
}

This is common enough that the React team made it's own package that gives us a global PropTypes object. You can pull that into your project and use it the same way we just did, but you don't have to write your own function for it.

// all we need to do is this:
SayHello.propTypes = {
  firstName: PropTypes.string,
  lastName: PropTypes.string
}

By default, props aren't required, so if our instance only gives a value for one of them, only that one error will show. You can set a value to be required:

SayHello.propTypes = {
  firstName: PropTypes.string.isRequired
}

Prop types aren't run in production versions of React. So these errors will only work for development builds.

Understand and use interpolation in JSX

JS and JSX can switch back and forth fairly often. A key thing to remember is that between the {} in JSX, we can only put JS expressions. Whatever goes in those braces will be passed almost directly to the createComponent function, so it has to stay fairly straight forward. There can still be some slightly more complex examples that work, though.

// For example, this works! It might seem like looping but still evaluates to a final value of an expression
function CharacterCount({text}) {
  return (
    <div>
      {`The text "${text}" has `}
      {text.length ? <strong>{text.length}</strong> : 'No'}
      {' characters'}
    </div>
  )
}

const element = (
  <>
    <CharacterCount text="Hello World" />
    <CharacterCount text="" />
  </>
)

Rerender a React app

With React hooks you often won't need to directly rerender your app, but it is possible using JS. We simply pass the render call in a function that is then called anytime we want it rendered.

function tick() {
  const time = new Date().toLocaleTimeString();
  const element = (
    <div>
      <input value={time} />
    </div>
  )

  ReactDOM.render(element, rootElement);
}

setInterval(tick, 1000);

For this example, we let React re-render the items instead of using a template string. If we used a string, the entire section would be re-written every time, so any focus styles would be lost and static content is re-rendered when it doesn't need to be. React checks everything in the function and only re-renders whatever has changed. So this way, we could pass in static content or allow users to focus on things, and those will stay the same and only the updated content will change.

Style React components with className and inline styles

Can apply CSS classes with className, or can use the style tag which takes an object of CSS properties with camelCased names (instead of kebab-cased names like they are in straight CSS).

If you want to factor out common styles into a component, make sure you're careful about how you do it! Properties will overwrite each other if just listed straight, so you want to be specific when you create these.

<div className="box" style={{backgroundColor: 'lightblue'}} />
// factored out styles
function Box({className = '', style, ...rest}) {
  return (
    <div 
      className={`box ${className}`}
      style={{fontStyle: 'italic', ...style}}
      {...rest}
    />
  )
}
<Box className="box--small" style={{backgroundColor: 'pink'}} />

// another option would be to allow users to provide a size, and then use that in our function instead of them needing to know the class name
const sizeClassName = size ? `box--${size}` : '';
className = {`box ${className} ${sizeClassName}`};
<Box size="small" />

className is used in React because it's how you actually access the class variables in a DOM object. Also, class is a reserved keyword, so using className doesn't cause any errors.

Use event handlers in React

Events in React are very similar to native events, but React does some optimization on top. Supported events can be found on the docs page here. Below is a simplified way to test events, since we haven't covered state yet.

What's nice with React events is that we call the actions on the item that it's attached to, so it's logical to see what a component does.

// this object is holding our state - the things that will update on events
const state = {eventCount: 0, username: ''}

    function App() {
      // the setState function is one we defined below
      function handleClick() {
        setState({eventCount: state.eventCount + 1})
      }

      function handleChange(event) {
        setState({username: event.target.value})
      }

      return (
        <div>
          <p>There have been {state.eventCount} events.</p>
          <p>
            <button onClick={handleClick}>Click Me</button>
          </p>
          <p>You typed: {state.username}</p>
          <p>
            <input onChange={handleChange} />
          </p>
        </div>
      )
    }
    // this is assigning the new values to our current object, effectively update the state
    function setState(newState) {
      Object.assign(state, newState)
      renderApp()
    }
    // this just tells React to re-render the DOM
    function renderApp() {
      ReactDOM.render(<App />, document.getElementById('root'))
    }

    renderApp()

Manage state in a component with useEffect hook

Setting a value isn't as simple as just reassigning a variable - our UI won't update, and if we re-render the component the updated data is gone. So React has a hook to handle data (state) called useState, and it returns an array containing a default value and a function to update that value.

function Greeting() {
  // using destructuring here for less lines of code
  const [name, setName] = React.useState('')
  const handleChange = event => setName(event.target.value)
  return (
    <div>
      <form>
      // JSX version of for is htmlFor
        <label htmlFor="name">Name: </label>
        <input value={name} onChange={handleChange} id="name" />
      </form>
      {name ? <strong>Hello {name}</strong> : 'Please type your name'}
    </div>
  )
}

Manage side-effects in component with useEffect hook

If we want that data to persist (using something like local storage) on page reloads, this is called a side effect and we can use React's useEffect hook to manage that. Placed inside a component, this hook is called every time the component is rendered.

function Greeting() {
  // since our effect hook deals with this state value, we want to update our default to check local storage first
  const [name, setName] = React.useState(
    window.localStorage.getItem('name') || ''
  )
  React.useEffect(() => {
    window.localStorage.setItem('name', name)
  })
}

Use a lazy initializer with useState

Sometimes we need to do a calculation for a state value on the initial render, but we don't want it to run every time the component is rendered. Like our above example - we want to check localStorage when we first load the component, but don't need to check it every time we type a letter into the input.

To solve this, instead of passing a direct value into useState we can instead pass a function that returns a value. This way, React only calls the function when it needs the initial value, and we save some potentially expensive calls from happening.

const [name, setName] = React.useState(() => window.localStorage.getItem('name') || '')

Manage useEffect dependency array

Similarly, useEffect hooks can also be called more often than they need to be. These also run every time the component is rendered and if we're updating something in the component that doesn't affect the hook we don't need it to run.

For useEffect, we can add a second option to the hook - a dependency array. Not every effect hook will need this, but it's nice to have when it can use it. This array tells the hook what state values need to stay in sync. So when that state value changes it calls the effect hook, but won't call it when another state option changes.

React.useEffect(() => {
  window.localStorage.setItem('name', name)
}, [name])

A benefit of this is that we also get syncs with local storage more often. Without the dependency array, our UI will update each time we make a change, but if we then refresh the page the updates are lost and it'll go back to whatever was last saved in local storage (when the page last loaded). With the dependency array telling the hook that it relies on the name state value, each time name is updated it will call the effect hook.

Create reusable custom hooks

Since hooks are fairly straight forward JavaScript, we can refactor these to be their own functions, which can then be reusable in multiple components. The examples above would then look like this:

// instead of setting a specific key name and default, we can pass them as parameters so they can be updated from the component
function useLocalStorageState(key, defaultValue = '') {
      const [state, setState] = React.useState(
        () => window.localStorage.getItem(key) || defaultValue,
      )
      // notice the dependency array is different here - since our key isn't hard coded anymore, this hook now needs both values in the dependency array
      React.useEffect(() => {
        window.localStorage.setItem(key, state)
      }, [key, state])
      // here we make sure to return the state variables so we can call them in the components as we'd expect to
      return [state, setState]
    }

    function Greeting() {
      // then we can destructure and rename our values just as we did before, but using the custom hook
      const [name, setName] = useLocalStorageState('name')
      // ...rest of function
    }

Custom hooks don't have to start with the word use - they can have any name we want. However, starting it with the name use allows eslint plugins to enforce some conventions, since the React standard hooks also begin with use.

Manipulate the DOM with refs

Say we need to access a DOM node we create with JSX. Since these aren't true HTML until after React has rendered it, we need a way to ask React for the reference to the node it will create.

To do this, we pass a ref property to the element we want to be able to control. This gives us an object with a mutable current property. This requires us to call React.useRef() to initialize this object.

To update our ref property, we'll need to run some functions in a useEffect hook. We also want to add a return function to our useEffect call, to allow JS to properly garbage collect the node once we're done with it, clearing event handlers and other data we don't need anymore.

Finally, we'll also want to make sure we put a dependency array on our useEffect call, so we're not calling an init and destroy function on every render of our component. Since we don't want any of the options inside to change (nothing relies on the state of the component), we can use an empty array. This effectively says we want to run it when we initially mount it to the page, and when we unmount it.

This example shows an option for what this would look like, to apply a specific CSS library option to a node.

function Tilt({children}) {
  // this will actually initialize the ref object
  const tiltRef = React.useRef()

  React.useEffect(() => {
    // we set the current value in here so once React renders this component, we can access the DOM node
    const tiltNode = tiltRef.current
    const vanillaTiltOptions = {
      max: 25,
      speed: 400,
      glare: true,
      'max-glare': 0.5,
    }
    VanillaTilt.init(tiltNode, vanillaTiltOptions)
    // when we get rid of this node, this will clear out all the event listeners and anything else related to that DOM node
    return () => {
      tiltNode.vanillaTilt.destroy()
    }
  }, [])

  return (
    // we call ref on the DOM node we want to control
    <div ref={tiltRef} className="tilt-root">
      <div className="tilt-child">{children}</div>
    </div>
  )
}

Understand React hook flow

Key things to know:

  • useState hooks are only called on initial render - so any logic in them is only run when it's first created.
  • useEffect hooks will call a function that's returned (the cleanup functions) before it calls the setup functions when the component is re-rendered.
  • Otherwise, things will run in the order they appear in the code.
  • useEffect functions with an empty dependency arrays won't be called on re-renders - since their dependency array is empty, it knows nothing in it will be updating so it won't run after it's initialized.

A good diagram of how this looks: React hook flow diagram, for when things mount, update, and unmount

Also a code sample to copy and play around with, to see it in action (open index.js and have the console tab open): https://codesandbox.io/s/beginner-react-egghead-flow-example-mi64d

Make basic forms

Forms in React are fairly similar to forms in HTML - it's just how we get the data that's a touch different.

To capture the submit event, we want to put an onSubmit event on the form element, and pass it a handler function to deal with that. This gives us the standard HTML event, so we can do things like call preventDefault and similar just like you would in vanilla JS. We put the call on form and not button so that hitting enter after an input field also works, just like it natively would.

Any elements in a form that have an id or name will be listed as properties in the elements section of a form. So the best way to grab form data is by calling that id / name. There are other methods that work as well.

// only need this if you're using the ref
// const usernameInputRef = React.useRef()

function handleSubmit(event) {
  event.preventDefault()
  // A few options:
  // We could use document.querySelector('input').value, but that breaks the idea of keeping our component contained, so least recommended

  // Either of these would work, but aren't great since it relies on render order and could easily be mixed up
  // const username = event.target[0].value
  // const username = event.target.elements[0].value

  // if you needed the ref for another reason, could also use refs here, and that works fine - just a little extra work
  // const username = usernameInputRef.current.value

  // this is the recommended way, since we're already associating the label and input together for accessibility
  const username = event.target.elements.usernameInput.value
}

return (
  <form onSubmit={handleSubmit}>
    <div>
      <label htmlFor="usernameInput">Username:</label>
      // if you want to use refs, add 'ref={usernameInputRef}' to the input
      <input id="usernameInput" type="text" />
    </div>
    <button type="submit">Submit</button>
  </form>
)

Make dynamic forms

If we need to know the status of an input field dynamically (as it's being updated), we can use the onChange event. This would be good for error messages when something's happening.

const [username, setUsername] = React.useState('')
const isLowerCase = username === username.toLowerCase()
const error = isLowerCase ? null : 'Username must be lower case'

function handleChange(event) {
  // since this is called directly on our input, can just use the event target to update our state value
  setUsername(event.target.value)
}

return (
  <form onSubmit={handleSubmit}>
    <div>
      <label htmlFor="usernameInput">Username:</label>
      <input id="usernameInput" type="text" onChange={handleChange}/>
    </div>
    <div style={{color: 'red'}}>{error}</div>
    <button disabled={Boolean(error)} type="submit">Submit</button>
  </form>
)

Controlling form values

If we wanted to transform what a user types that's easy, but we need to make sure to update the UI as well, so everything stays in sync. This is also easy - just make sure to add the value attribute to our input. This tells React that we want to control that item programmatically, so it won't try to do it itself anymore. Then when we update the state value, the UI will show the same value.

const [username, setUsername] = React.useState('')

function handleChange(event) {
  setUsername(event.target.value.toLowerCase())
}

return (
  <form onSubmit={handleSubmit}>
    <div>
      <label htmlFor="usernameInput">Username:</label>
      <input 
        id="usernameInput" 
        type="text" 
        onChange={handleChange}
        value={username}
      />
    </div>
    <button type="submit">Submit</button>
  </form>
)

Use React error boundaries to handle errors in components

Error boundaries have to be classes in React. You can create one yourself or use a library.

Error boundaries can be rendered anywhere in the component, but the location does matter. It handles any errors that happen in it's descendents - which also means it'll render it's error data in place of it's descendents. So anything inside will not render if an error happens. Often this is what you want - it's just good to know that's how it works. You can also have multiple ones, if needed.

Error boundaries specifically work for errors that happen in React callstack - not in event handlers or async callbacks. Just ones that happen in render or hooks.

// this is the library Kent uses for errors
const ErrorBoundary = ReactErrorBoundary.ErrorBoundary

// To do this ourselves, it would look something like this:
// class ErrorBoundary extends React.Component {
//   state = {error: null}
//   static getDerivedStateFromError(error) {
//     return {error}
//   }
//   render() {
//     const {error} = this.state
//     if (error) {
//       if . included in a component name, it's ok to start with lower case
//       return <this.props.FallbackComponent error={error} />
//     }
//     // this will be the same as the props passed into it as children
//     return this.props.children
//   }
// }

// good to make a fallback component, so if something goes wrong it shows this
function ErrorFallback({error}) {
  return (
    <div>
      <p>Something went wrong:</p>
      <pre>{error.message}</pre>
    </div>
  )
}

function Bomb() {
  throw new Error('💥 CABOOM 💥')
}

function App() {
  const [explode, setExplode] = React.useState(false)
  return (
    <div>
      <div>
        <button onClick={() => setExplode(true)}>💣</button>
      </div>
      <div>
        <ErrorBoundary FallbackComponent={ErrorFallback}>
          {explode ? <Bomb /> : 'Push the button Max!'}
        </ErrorBoundary>
      </div>
    </div>
  )
}
ReactDOM.render(<App />, document.getElementById('root'))

Use key prop when rendering a list

When mapping over arrays or creating lists of things, you need to provide a key so that React knows which item you're actively working on. Keys need to be unique to each item - often an id. If we don't provide a key (or multiple items have the same key), we can have bugs in the UI where things get mixed up on re-renders.

Be careful with trying to use an index as a key - if you're modifying the array between renders, you can still have bugs! It's best to have a unique key for each item stored in the array.

Another benefit of using a unique key - focus will move around with the item you're focused on if you use a key. Using index or no key won't preserve your interactions with that item if it moves during the interaction.

<ul>
  {items.map((item) => (
    <li key={item.id}>
      // ...
    </li>
  ))}
</ul>

Lifting and colocating state

You want your state values as close to the code that's using it as possible. But if you've got sibling components that need to access that state, you "lift" it to the closest parent component they both have, then pass the state and changeState values to the component when you call them as props.

Pushing state back down (colocating) is a good habit when your needs change. If you no longer need the state in siblings or the parent, you should move it into the component that uses it.

Make HTTP requests

To make async calls (like HTTP requests), those go in a useEffect hook. Otherwise, fairly similar to standard JS.

function PokemonInfo({pokemonName}) {
  // this is the state we use to hold our data from the API
  const [pokemon, setPokemon] = React.useState(null)
// use the effect hook here in the function that needs the async data
  React.useEffect(() => {
    if (!pokemonName) {
      return
    }
    fetchPokemon(pokemonName).then(pokemonData => {
      setPokemon(pokemonData)
    })
  }, [pokemonName])

  if (!pokemonName) {
    return 'Submit a pokemon'
  }

  if (!pokemon) {
    return '...'
  }

  return <pre>{JSON.stringify(pokemon, null, 2)}</pre>
}

function App() {
  // this state is for the form input
  const [pokemonName, setPokemonName] = React.useState('')

  function handleSubmit(event) {
    event.preventDefault()
    setPokemonName(event.target.elements.pokemonName.value)
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label htmlFor="pokemonName">Pokemon Name</label>
        <div>
          <input id="pokemonName" />
          <button type="submit">Submit</button>
        </div>
      </form>
      <hr />
      <PokemonInfo pokemonName={pokemonName} />
    </div>
  )
}

// our actual API call
function fetchPokemon(name) {
  const pokemonQuery = `
    query ($name: String) {
      pokemon(name: $name) {
        id
        number
        name
        attacks {
          special {
            name
            type
            damage
          }
        }
      }
    }
  `

  return window
    .fetch('https://graphql-pokemon.now.sh', {
      // learn more about this API here: https://graphql-pokemon.now.sh/
      method: 'POST',
      headers: {
        'content-type': 'application/json;charset=UTF-8',
      },
      body: JSON.stringify({
        query: pokemonQuery,
        variables: {name},
      }),
    })
    .then(r => r.json())
    .then(response => response.data.pokemon)
}

Handle HTTP errors

If we make an error, it often won't show in the UI, leaving users confused. So we need to handle those errors ourselves. In the previous example, we can add an error state to our fetchPokemon call in the pokemonInfo function, and add a state variable to store error data. Then we should update the way we determine what to show the user, so depending on what stage the API call is in, it shows the right information. The updated function would look like this:

function PokemonInfo({pokemonName}) {
  // now we have a status variable to let us know what stage of the call we're in
  const [status, setStatus] = React.useState('idle')
  // the pokemon variable to store our success data
  const [pokemon, setPokemon] = React.useState(null)
  // and an error variable to store our error data
  const [error, setError] = React.useState(null)

// then throughout our hook, we update the status depending on what's happening, and determine what to show in the UI based on the status
  React.useEffect(() => {
    if (!pokemonName) {
      return
    }
    setStatus('pending')
    fetchPokemon(pokemonName).then(
      pokemonData => {
        setStatus('resolved')
        setPokemon(pokemonData)
      },
      errorData => {
        setStatus('rejected')
        setError(errorData)
      },
    )
  }, [pokemonName])

  if (status === 'idle') {
    return 'Submit a pokemon'
  }

  if (status === 'pending') {
    return '...'
  }

  if (status === 'rejected') {
    return 'Oh no...'
  }

  if (status === 'resolved') {
    return <pre>{JSON.stringify(pokemon, null, 2)}</pre>
  }
}

Install and use React devtools

The dev tools provide two new tabs - Components and Profiler. The Components tab shows you data about the currently rendered view including current state. And the Profiler will show you when renders happen and what triggered the render.

Typing $r in the console while you have the Components tab open gives you info on the currently selected component.

← Back to the Note Garden