Skip to content

Instantly share code, notes, and snippets.

@indiesquidge
Last active January 19, 2024 15:57
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save indiesquidge/476b578ba2bd96d391af5651a4102ad7 to your computer and use it in GitHub Desktop.
Save indiesquidge/476b578ba2bd96d391af5651a4102ad7 to your computer and use it in GitHub Desktop.
Personal notes while working through Advanced React: https://courses.reacttraining.com/p/advanced-react

Advanced React by React Training

Personal notes while working through Advanced React: https://courses.reacttraining.com/p/advanced-react

Granted this is a contrived example, but it's still something I took notice to: in those "Advanced React" videos I've been watching, Ryan Florence codes very slowly, and does not make one quick change and jump back to the browser to see what changed.

He stops and thinks. He asks himself (or the viewer) questions. He wonders what different approaches might look like. He double checks his work. He is in no rush.

And it is so much more intelligent of a way to program.

The way Ryan Florence handles this course is unlike most tutorials I find. He is lighthearted and funny throughout the whole series, and that makes watching and listening to him all the more pleasurable. Code is often taken far too seriously (I am certainly guilty of this), but Ryan is able to take complex topics and make them entertaining. His pairing of humor and insight is invaluable, and I have learned more than just how to code from watching him.

He is also highly experimental in his way of programming, and uses code as a medium of thought process. When he has a crazy idea, he tries it out; when something doesn't work, he removes it and tries a different path; when his spidey-sense is tingling, he double checks his work and is not unduly quick to mash the save button and see what renders.

I really love the way these exercises are set up. Rather than having you implement something from a blank canvas, many of the exercises come with some code that is already "using" the code we are meant to implement. What I mean by this is that the consumption of the API has already been created for us—what we need to do is implement the internals of the API.

This provides a great starting point and also helps us in another way. When writing code on my own now (i.e. starting from a "blank canvas"), I have often found myself writing my API before I have even decided how I'd like to interact with it. After watching this series, I have started to write what I would expect my API to provide before actually writing it.

Section 01 - Imperative vs. Declarative

The topic of this section is to discuss how to create React components that isolate imperative operations. We are shown how to create "behavioral components": components that don't render any UI but just provide some sort of behavior or state.

Take Aways

"One of the things I love about React is that it lets me eliminate time in my code; I don't have to think about time, I just have to think about snapshots in time. At any time in a React app, we can look at the state of the app and then look at the render methods of our components, and we should be able to predict what the screen is going render." — Ryan Florence

"You can tell when you've got a good abstraction not just when you can add code easily, but when you can remove code easily." — Michael Jackson

The "behavioral" component <PinScrollToBottom /> is a great example of a component that has this kind of nice abstraction. In order to remove the behavior it provides, one need only to remove the component and render its children as they are. That's it. The code is very easily removable without breaking any functionality, and that's wonderful abstraction to work with when building.

This idea of "behavioral" React Components—or components that don't render anything but just provide some sort of behavior or state—is still quite novel to me. The examples in this section are intriguing (especially the <Tone /> component from the lecture, which I initially saw a while back at Ryan Florence's React Rally 2016 talk), and I love how wonderful they package imperative code, but I still find myself struggling to fit the concepts into my own development. I suppose this will just take time, and having the knowledge that they exist will hopefully allow me to recognize when and where they can be used.

Perhaps a good heuristic to slowly train myself to think about "behavioral" components would be to isolate my imperative code in a (temporarily named) doImperativeStuff method, and then figure out if I could instead just be "reacting" to state changes in componentDidMount and componentDidUpdate.

componentDidUpdate can be thought of as React's way of saying "okay, I've updated this component's state (and props) and I've updated the DOM in response to the changed state (and props), is there anything else you would like to react to? Is there anything else you'd like to do given the state of this app?". It is our chance to "do imperative stuff". It's React giving us a chance to participate in updating the app in response to a change in state.

TL;DR

Behavioral React Components: components that don't render anything but just provide some sort of behavior or state.

componentDidMount and componentDidUpdate are wonderful React life cycle hooks that can be used together in order to isolate and perform imperative operations. They are React's way of giving us a chance to participate in updating the app in response to a change in state.

Exercise Solution Code

// My solution
class PinScrollToBottom extends Component {
  state = {
    scrollHeight: undefined
  }

  componentDidMount () {
    const { scrollHeight } = document.documentElement.scrollHeight
    window.scrollTo(0, scrollHeight)
    this.setState({ scrollHeight })
  }

  componentDidUpdate (prevProps, prevState) {
    const { scrollTop, scrollHeight, clientHeight } = document.documentElement

    if (scrollHeight !== prevState.scrollHeight) {
      this.setState({ scrollHeight })
    }

    if (scrollTop === scrollHeight - clientHeight) {
      window.scrollTo(0, scrollHeight)
    }
  }

  render () {
    return this.props.children
  }
}
// React Training's solution
class PinScrollToBottom extends Component {
  componentDidMount () {
    this.scroll()
  }

  componentDidUpdate () {
    this.scroll()
  }

  componentWillUpdate () {
    const { clientHeight, scrollTop, scrollHeight } = document.documentElement
    this.scrolledUp = clientHeight + scrollTop < scrollHeight
  }

  scroll () {
    if (!this.scrolledUp)
      window.scrollTo(0, document.documentElement.scrollHeight)
  }

  render () {
    return this.props.children
  }
}

Thoughts on Implementations

I had to spend some time getting familiar with scrollHeight, clientHeight, scrollTop, and scrollTo, as these were methods and properties I had not previously worked with. Even after reading the MDN pages for these methods and properties, I had to experiment myself with them to fully grasp what they did.

In my experimentation, I found that scrollTop === scrollHeight - clientHeight was true whenever I was scrolled to the bottom of the page. This obviously felt like a useful predicate to determine if I should be calling scrollTo and updating the scroll to the bottom of the page (i.e. scrollHeight). I also found that clientHeight, by its definition, never changed on a scroll event (it only changes when I change the height of my browser window).

The biggest mental roadblock I ran into was around when and how I should be checking and updating the values of the scrollHeight and/or scrollTop. (Even just that sentence has so many ands and ors when I write it out now, ha!) It became clear that I needed to maintain some sort of state.

In hindsight, after seeing the React Training solution, it appears that state may not have been the best way to handle this, as setting state will run a component through a render life cycle, and before I had my conditionals straight this resulted in many a stack overflow and unresponsive browser tabs.

The solution provided by React Training is definitely more elegant than what I came up with. I love that both componentDidMount and componentDidUpdate simply make a call to scroll, and there is no component state.

The more I look at it, the more my solution and the solution React Training provides seem similar. If I moved the if statement that makes the call to scrollTo to a scroll method, then the only difference between the two solutions is that mine uses scrollHeight state and React Training's uses componentWillUpdate (which can't update state) and an instance method scrolledUp.

I'm endlessly impressed with the ever-growing number of problems React is capable of handling. Reactively rendering UI is one thing, but creating components that allow us to just provide behavior or state, and implementing them in a way that eliminates the need to think about time, is outstanding.

Section 02 - Compound Components

The topic of this section is on creating compound components and passing around implicit props in order to make components extendable and changeable in ways other than an ever-growing list of props.

Take Aways

"Our [component] children are really just data. Just because someone told us to render these children doesn't mean we have to render them in their current form. We can map over them, inspect them, and clone them to render something different based on the data the give us." — Ryan Florence

In the beginning of the solution video, Ryan Florence talks about his preferred building process in React. He likes to start with building out state and then rendering that state. This way, he can just swap that state around to make sure that he's got things working right before adding in the event handlers. This helps him keep things declarative.


The third or fourth time watching the lecture video I had an epiphany moment when I really understood why compound components are useful.

When a component owns all of the rendering, when you have to do something new or different (e.g. disabling, changing render order, etc.) you end up having to create and expose a new prop. This is more or less the same as how we used to create elements with something like jQueryUI, which handled all rendering on an initial setup, and then exposed some kind of options object to give some instruction on what to render. But what if in React, instead of having one big component responsible for rendering, we create a bunch of components that "compound" together in order to get the unique interaction we're looking for?


<div onClick={isDisabled ? null : () => this.handleClick} />

React doesn't add and remove event listeners like the above onClick prop, but instead it delegates one click from the top of the document to all of its virtual DOM elements. So it will just capture the click and check if there is actually a handler on the virtual element and only call the handler if it's present. But as the app switches back and forth between disabled and enabled, React isn't adding and removing event listeners, so code like the above snippet is actually really cheap in terms of performance.

TL;DR

Implicit state: non app-level state that the product developer doesn't care about or see in order to use the component API successfully; in React, this is generally accomplished by using React.Children.map and React.cloneElement in order to implicitly pass state-derived or event-callback props to children

Component components: design architecture that helps avoid having one component in charge of too much rendering or state; the pattern encourages components to have limit responsibility, which helps identify where certain state and rendering should be owned in a component composition, rather than exposing all options at a single, top-level component with an ever-growing list of props

Compound components generally have some level of implicit prop passing in order to accomplish the task of limited responsibility. Rather than treating children components as something to be immediately rendered, compound components clone and extend children components in order pass along useful data.

Ryan Florence's state-first build process:

  • state
  • render
  • swap state around manually for testing
  • create event handlers

Exercise Solution Code

// My solution
class RadioGroup extends Component {
  state = {
    activeIndex: 0
  }

  selectButtonIndex = activeIndex => {
    this.setState({ activeIndex })
  }

  render () {
    return (
      <fieldset className='radio-group'>
        <legend>{this.props.legend}</legend>
        {React.Children.map(this.props.children, (child, index) => {
          return React.cloneElement(child, {
            isActive: index === this.state.activeIndex,
            onSelect: () => this.selectButtonIndex(index)
          })
        })}
      </fieldset>
    )
  }
}

class RadioButton extends Component {
  render () {
    const { isActive, onSelect } = this.props
    const className = 'radio-button ' + (isActive ? 'active' : '')
    return (
      <button className={className} onClick={onSelect}>
        {this.props.children}
      </button>
    )
  }
}
// React Training's solution
class RadioGroup extends Component {
  state = {
    value: this.props.defaultValue
  }

  render() {
    const children = React.Children.map(this.props.children, (child) => {
      return React.cloneElement(child, {
         isActive: child.props.value === this.state.value,
         onSelect: () => this.setState({ value: child.props.value })
      })
    })
    return (
      <fieldset className="radio-group">
        <legend>{this.props.legend}</legend>
        {children}
      </fieldset>
    );
  }
}

class RadioButton extends Component {
  render() {
    const { isActive, onSelect } = this.props
    const className = "radio-button " + (isActive ? "active" : "");
    return (
      <button className={className} onClick={onSelect}>
        {this.props.children}
      </button>
    );
  }
}

Thoughts on Implementations

This section's lecture video was actually part of the preview for this course, so I had watched it when the course was initially announced and implemented my own version of actual radio elements (i.e. <input type="radio" />) for practice. I like that the exercise here uses radio buttons as it's example, as actual radio elements are a perfect real-world use-case for this kind of thing.

There is not much of a difference between my solution and the one provided by React Training, but I did notice that the defaultValue prop was not actually included on <RadioGroup /> in the solution source code. I could have just added it myself, but instead decided to follow the directions to a tee (i.e. not touching <App /> at all) and wondered if I could implement a solution without defautlValue.

I ended up just using the child index as the active value, very similar to how the lecture code was written for tabs. Doing this is more or less the only difference between my solution and the provided solution.

The only other differences are cosmetic, but perhaps one worth mentioning is that I inline rendered the React.Component.map call and the provided solution associates this with a local children variable. It's nothing more than a matter of taste, but I do think I prefer the provided solution over my own inline rendering.

Section 03 - Context

Take Aways

With the added overhead React context brings, and with the core team discouraging its use, I'm unconvinced that using it makes for a better app API. The lecture on refactoring <Tab /> to use context was more code, a new API, and merely provided the short-sighted benefit of avoiding passing activeIndex and onSelectTab as props. The proposition of using context to alleviate a strict parent-child relationship between compound components seems a bit contrived, and ultimately adds more abstraction and opaqueness to the code. For example, in the lecture, why not just add a className prop to the <TabList> and <TabPanel> components?

Perhaps the lecture refactor was a contrived example because of how small the benefit was to using context. I am thankful for the context API in popular libraries like Redux and React Router, as they save me from having to thread props to deeply nested children. However, I am not sold on it being a pattern used liberally or without a full understanding of how to isolate its ostensibly unstable API with higher order components. It should be seen as a second-class citizen to props and state, and including it in production-level code should not be taken lightly.

That said, experimentation with context (especially when used with higher order components) can be a very eye-opening endeavour. It will help expose how libraries that use context do so intelligently and without the app developer needing to touch the context themselves.

TL;DR

React context is implemented in two parts: a provider and a consumer.

The provider component uses the static childContextTypes property in order to specify the name, type, and requirement of the context it plans to provide, and then makes use of the getChildContext life cycle hook to actually provide a context object.

The consumer component uses the static contextTypes property to specific the name, type, and requirement of the context it plans to consume. If the context properties on the provider match the context properties on the consumer, the consumer can make use of the properties available within the component on this.context.

Context can then be used to share and manage state between components regardless of any intermediary UI. It acts as a bit of a wormhole between provider and consumer, breaking the normal boundaries of state management between components via props.

While context makes it rather easy to pass state around to arbitrary levels of your component hierarchy, it also obfuscates where a component may be getting its state and props from. Context is also warned against by the React team due to its instable and change-prone api.

Exercise Solution Code

class AudioPlayer extends React.Component {
  static childContextTypes = {
    play: PropTypes.func.isRequired,
    pause: PropTypes.func.isRequired,
    jumpForward: PropTypes.func.isRequired,
    jumpBack: PropTypes.func.isRequired,
    jump: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired,
    duration: PropTypes.number.isRequired,
    currentTime: PropTypes.number.isRequired
  }

  state = {
    isPlaying: false,
    duration: 0,
    currentTime: 0
  }

  getChildContext () {
    const { isPlaying, duration, currentTime } = this.state
    return {
      play: this.play,
      pause: this.pause,
      jumpForward: this.jumpForward,
      jumpBack: this.jumpBack,
      jump: this.jump,
      isPlaying,
      duration,
      currentTime
    }
  }

  play = () => {
    this.audio.play()
    this.setState({ isPlaying: true })
  }

  pause = () => {
    this.audio.pause()
    this.setState({ isPlaying: false })
  }

  jumpForward = seconds => {
    this.audio.currentTime += seconds
  }

  jumpBack = seconds => {
    this.audio.currentTime -= seconds
  }

  jump = seconds => {
    this.audio.currentTime = seconds
  }

  render () {
    return (
      <div className='audio-player'>
        <audio
          src={this.props.source}
          onTimeUpdate={() => this.setState({ currentTime: this.audio.currentTime })}
          onLoadedData={() => this.setState({ duration: this.audio.duration })}
          onEnded={() => this.setState({ isPlaying: false, currentTime: 0 })}
          ref={n => { this.audio = n }}
        />
        {this.props.children}
      </div>
    )
  }
}

class Play extends React.Component {
  static contextTypes = {
    play: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired
  }

  render () {
    const { play, isPlaying } = this.context

    return (
      <button
        className='icon-button'
        onClick={play}
        disabled={isPlaying}
        title='play'
      ><FaPlay /></button>
    )
  }
}

class Pause extends React.Component {
  static contextTypes = {
    pause: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired
  }

  render () {
    const { pause, isPlaying } = this.context

    return (
      <button
        className='icon-button'
        onClick={pause}
        disabled={!isPlaying}
        title='pause'
      ><FaPause /></button>
    )
  }
}

class PlayPause extends React.Component {
  static contextTypes = {
    isPlaying: PropTypes.bool.isRequired
  }

  render () {
    return this.context.isPlaying ? <Pause /> : <Play />
  }
}

class JumpForward extends React.Component {
  static contextTypes = {
    jumpForward: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired
  }

  render () {
    const { jumpForward, isPlaying } = this.context

    return (
      <button
        className='icon-button'
        onClick={() => jumpForward(10)}
        disabled={!isPlaying}
        title='Forward 10 Seconds'
      ><FaRepeat /></button>
    )
  }
}

class JumpBack extends React.Component {
  static contextTypes = {
    jumpBack: PropTypes.func.isRequired,
    isPlaying: PropTypes.bool.isRequired
  }

  render () {
    const { jumpBack, isPlaying } = this.context

    return (
      <button
        className='icon-button'
        onClick={() => jumpBack(10)}
        disabled={!isPlaying}
        title='Back 10 Seconds'
      ><FaRotateLeft /></button>
    )
  }
}

class Progress extends React.Component {
  static contextTypes = {
    duration: PropTypes.number.isRequired,
    currentTime: PropTypes.number.isRequired,
    jump: PropTypes.func.isRequired
  }

  jumpProgress = e => {
    const { duration, jump } = this.context
    const rect = e.currentTarget.getBoundingClientRect()
    const relativeClickPosition = e.clientX - rect.left
    const percentage = relativeClickPosition / rect.width
    const updatedTime = percentage * duration

    jump(updatedTime)
  }

  render () {
    const { currentTime, duration } = this.context
    const progressPercentage = ((currentTime / duration) * 100) || 0

    return (
      <div
        className='progress'
        onClick={this.jumpProgress}
      >
        <div
          className='progress-bar'
          style={{
            width: `${progressPercentage}%`
          }}
        />
      </div>
    )
  }
}
class AudioPlayer extends React.Component {
  static childContextTypes = {
    audio: object
  }

  state = {
    isPlaying: false,
    duration: null,
    currentTime: 0,
    loaded: false
  }

  getChildContext() {
    return {
      audio: {
        ...this.state,
        setTime: (time) => {
          this.audio.currentTime = time
        },
        jump: (by) => {
          this.audio.currentTime = this.audio.currentTime + by
        },
        play: () => {
          this.setState({ isPlaying: true })
          this.audio.play()
        },
        pause: () => {
          this.setState({ isPlaying: false })
          this.audio.pause()
        }
      }
    }
  }

  handleTimeUpdate = (e) => {
    this.setState({
      currentTime: this.audio.currentTime,
      duration: this.audio.duration
    })
  }

  handleAudioLoaded = (e) => {
    this.setState({
      duration: this.audio.duration,
      loaded: true
    })
  }

  handleEnded = () => {
    this.setState({
      isPlaying: false
    })
  }

  render() {
    return (
      <div className="audio-player">
        <audio
          src={this.props.source}
          onTimeUpdate={this.handleTimeUpdate}
          onLoadedData={this.handleAudioLoaded}
          onEnded={this.handleEnded}
          ref={n => this.audio = n}
        />
        {this.props.children}
      </div>
    )
  }
}

class Play extends React.Component {
  static contextTypes = {
    audio: object
  }

  render() {
    return (
      <button
        className="icon-button"
        onClick={this.context.audio.play}
        disabled={this.context.audio.isPlaying}
        title="Play"
      ><FaPlay/></button>
    )
  }
}

class Pause extends React.Component {
  static contextTypes = {
    audio: object
  }

  render() {
    return (
      <button
        className="icon-button"
        onClick={this.context.audio.pause}
        disabled={!this.context.audio.isPlaying}
        title="Pause"
      ><FaPause/></button>
    )
  }
}

class PlayPause extends React.Component {
  static contextTypes = {
    audio: object
  }

  render() {
    const { isPlaying } = this.context.audio
    return isPlaying ? <Pause/> : <Play/>
  }
}

class JumpForward extends React.Component {
  static contextTypes = {
    audio: object
  }

  render() {
    return (
      <button
        className="icon-button"
        onClick={() => this.context.audio.jump(10)}
        disabled={!this.context.audio.isPlaying}
        title="Forward 10 Seconds"
      ><FaRepeat/></button>
    )
  }
}

class JumpBack extends React.Component {
  static contextTypes = {
    audio: object
  }

  render() {
    return (
      <button
        className="icon-button"
        onClick={() => this.context.audio.jump(-10) }
        disabled={!this.context.audio.isPlaying}
        title="Back 10 Seconds"
      ><FaRotateLeft/></button>
    )
  }
}

class Progress extends React.Component {
  static contextTypes = {
    audio: object
  }

  handleClick = (e) => {
    const { audio } = this.context
    const rect = this.node.getBoundingClientRect()
    const clientLeft = e.clientX
    const relativeLeft = clientLeft - rect.left
    audio.setTime((relativeLeft / rect.width) * audio.duration)
  }

  render() {
    const { loaded, duration, currentTime } = this.context.audio

    return (
      <div
        className="progress"
        ref={n => this.node = n}
        onClick={this.handleClick}
        onKeyDown={this.handleKeyDown}
      >
        <div className="progress-bar" style={{
          width: loaded ? `${(currentTime/duration)*100}%` : '0%',
        }}/>
      </div>
    )
  }
}

Thoughts on Implementations

For a while, I was having trouble passing the audio ref through context. Since the ref prop is not invoked until after the component is mounted, and getChildContext is only called when state or props are changed, passing the instance property this.audio through context will be undefined in any consumer component.

One solution I attempted was to set the audio element ref to an <AudioPlayer /> state property in componentDidMount, something like this.setState({ audio: this.audio }). I figured that since the ref would available after mount, this would work out fine.

But I then ran into issues where the <Play /> and <Pause /> components would not invoke a re-render when clicked, i.e., I could get the song to play and pause just fine thanks to the closures created around them, but disabling the buttons using audio.paused (which is what I had initially been using instead of an isPlaying state property) was not working because actions taken on the audio ref element passed down in context would not cause a re-render.

Another workaround I came up with was to create closures like play = () => this .audio.play() as instance methods in <AudioPlayer /> and then pass this.play in context. This approach rubbed me the wrong way and is a duplication of what already exists on the audio element itself, but I ended up choosing this and moving forward nevertheless.

I don't know why I didn't think of just inlining the functions on the getChildContext return object, but that is certainly the cleanest solution. I knew there had to be a better way than what I was doing, and continually found myself down a number of rabbit holes that made the solution appear far more difficult than it actually was.


Updating the <Progress /> on click was also challenging. The audio element's currentTime property in seconds, but the best I can get with a user click is a percentage of the duration. I had to write out a way of converting the progress percentage from the user click into a (rough) value in seconds, and ended up using the same approach as the solution video.


It's nice to see that as I progressed through this challenge I was hitting the same checkpoints that Ryan Florence seems to hit (albeit not quite as elegantly; my learning style was much more debugging and experimentation—the solution video made it all look so easy, ha!).

Video solution bits that I liked

  • I liked the spreading of state in getChildContext
  • I thought it interesting that Ryan Florence uses null values for state that is "unknown" on initial render in a component
  • I liked how all of the context was put on an audio object rather than as top-level properties
  • Liked the use of a generic jump function and the passing of negative values to jump backwards

My solution bits that I liked

  • I liked setting currentTime back to zero in onEnded callback, rather than leaving it with the progress bar filled in
  • I liked using event.currentTarget instead of using a ref for the progress bar click handler

Section 04 - Higher Order Components

This section shows off another way to handle the passing around of state and state management across an application. Unlike other methods we have seen in previous sections like the use of compound components, cloneElement, and context, the pattern described in section 04 introduces the concept of "higher order components" that can be used to make state sharing a bit more explicit.

Take Aways

The term "higher order component" is borrowed from the functional programming term "higher order function". A "higher order function" is a function that returns another function, generally used to compose function behavior together thanks to closure scopes. In a similar vein, a "higher order component" is a function that takes a component as an argument and return a new component with some extra behavior baked into it.

A higher order component is not a component that returns a component, because that is precisely what every React component does by default.

Interesting to think that using cloneElement can more or less do the same thing that higher order components can do, they are just less explicit about it. It is harder to see that <WithMedia><App /></WithMedia> is passing state between <WithMedia> and <App />—there is nothing inherently telling that the <App /> is using some state derived from <WithMedia> because all this looks like is a component hierarchy.

Something like withMedia(App) is a bit more telling; we can infer from the "decorative" nature of withMedia that it is doing something with App and returning something new.

"If you're used to using classical inheritance to clean up code and share behavior across an app, HOCs are a great substitute for that. You're not going to see people doing inheritance in React." — Ryan Florence

HOCs are a nice drop-in replacement for inheritance (actually closer to mixins or multiple-inheritance patterns).

The HOC pattern is something I've worked closely with in the last few months (using ReactRedux's conntect and ReactRouter's withRouter), but hadn't taken the time to study or build out myself.

It also appears to currently be the de facto way of passing state around a React application, although the "render prop/children as a function" pattern may usurp HOCs soon, as there are many pitfalls to HOCs.

TL;DR

Higher order components are functions that take a component as an argument and return a new component with some extra behavior baked into it.

HOCs A great way to share behavior across an app.

Watch out of common pitfalls of HOCs, including naming collisions when using multiple HOCs, confusion or indirection on which HOC is providing which props, and the number or precautions that must be taken when creating an HOC so as to not strip the original component of any its own implementation details.

Exercise Solution Code

// My solution

const withStorage = (key, default_) => Component => {
  return class WithStorage extends React.Component {
    state = {
      sidebarIsOpen: get(key, default_)
    }

    componentDidMount () {
      this.unsubscribe = subscribe(() => {
        this.setState({
          sidebarIsOpen: get('sidebarIsOpen')
        })
      })
    }

    componentWillUnmount () {
      this.unsubscribe()
    }

    render () {
      return <Component {...this.state} />
    }
  }
}
// React Training's solution

const withStorage = (key, default_) => (Comp) => (
  class WithStorage extends Component {
    state = {
      [key]: get(key, default_)
    }

    componentDidMount() {
      this.unsubscribe = subscribe(() => {
        this.setState({
          [key]: get(key)
        })
      })
    }

    componentWillUnmount() {
      this.unsubscribe()
    }

    render() {
      return (
        <Comp
          {...this.props}
          {...this.state}
          setStorage={set}
        />
      )
    }
  }
)

Thoughts on Implementations

"You'll see this across React generally [where] you'll get some sort of state as a prop and then you'll get a function to change that state. This is as fundamental as <input /> components having a value prop as well as an onChange prop." — Ryan Florence

Get some state, get a way to change that state.

Many of the video solution details were lost on me when I attempted a solution of my own at first.

  • I forgot to use a computed property key for my localStorage component state variable (which wouldn't harm the usefulness of the HOC, but would certainly make the source code difficult to understand for anyone looking at it)
  • I didn't think about the naming collisions of calling my HOC component argument Component and people using Component as a named export of React
  • I didn't consider replacing set from local-storage with a setter like setStorage passed in from our HOC, which removes forcing users to import and acknowledge the API of set
  • I didn't think about how to handle props that may be coming in on the component we are wrapping (need to spread in this.props on the returned component)
  • I didn't think about how a consumer who has imported the HOC-wrapped component will not have access to static properties on the wrapped component (although, to be fair, this one does seem a bit more nuanced, and it would seem as though many people have deferred to handling it with an external lib)
  • I didn't consider adding our wrapped component as a static property of our HOC (although, to be fair, in my own experience with HOCs I have opted to also export the base "unwrapped" class as a named export for testing purposes instead of relying on a static method)

Section 05 - Render Prop Callbacks

Section 05 was perhaps the most epiphanic section thus far, and really drives home what the point of this entire series is: abstracting behavior and state but still allowing the app to decide what to render based on that state.

The lecture does a better job explaining different types of state and what patterns are useful for managing both. It introduces the latest pattern for handling application-level state: render prop callbacks. This pattern addresses some of the issues we've witnessed using HOCs, and makes it much clearer what it means to compose state dynamically.

Take Aways

"Render callbacks and HOCs solve a lot of the same use-cases—in fact, every HOC could be implemented as a render callback. The difference between them is where the composition happens." — Ryan Florence

Render callbacks compose dynamically. They occur while we're rendering the element in the render method of a component.

HOCs compose statically. They occur while we are defining our component, not while we're rendering them.

App-level state: state that the developer actually cares about; state the developer would like to have the ability to interact with (e.g. HOCs)

Implicit state: state that isn't relevant to the developer or doesn't need to be seen or used or cared about by the developer (e.g. React.cloneElement, context)

"I'd like to be able to compose my state as easily as I compose my components together." — Ryan Florence

Render props are another great way to move behavior out of one component and into another so that it can be more sharable and reusable, but the real benefit is that it allows you to compose your state and share it across the app.

TL;DR

"Everything we've been doing [throughout this series] has been trying to give rendering back to the developer, and move [state management and imperative operations] somewhere else." — Ryan Florence

We named a prop "render", gave it a function, and in the component's render method that we put the render prop on, we call the function and pass it the component's state. So now we are not only able to compose the behavior and look of our components, but we can also compose the state.

Render props are a great way to share application-level state and behavior. If you wish to share implicit state, use context or React.cloneElement.

Exercise Solution Code

// My solution

class GeoAddress extends React.Component {
  state = {
    address: null
  }

  componentDidMount () {
    this.updateAddress(this.props.coords)
  }

  componentWillReceiveProps (nextProps) {
    if (nextProps.coords !== this.props.coords) {
      this.updateAddress(nextProps.coords)
    }
  }

  updateAddress = async (coords) => {
    const address = await getAddressFromCoords(coords.lat, coords.lng)
    this.setState({ address })
  }

  render () {
    return this.props.render(this.state.address)
  }
}

class App extends React.Component {
  render () {
    return (
      <div className='app'>
        <GeoPosition render={({ coords, error }) => {
          return error ? (
            <div>Error: {error.message}</div>
          ) : coords ? (
            <GeoAddress coords={coords} render={address => (
              <Map
                lat={coords.lat}
                lng={coords.lng}
                info={address}
              />
            )} />
          ) : (
            <LoadingDots />
          )
        }} />
      </div>
    )
  }
}
// React Training's solution

class GeoAddress extends React.Component {
  static propTypes = {
    coords: React.PropTypes.object
  }

  state = {
    address: null,
    error: null
  }

  componentDidMount() {
    if (this.props.coords)
      this.fetchAddress()
  }

  componentDidUpate(nextProps) {
    if (nextProps.coords !== this.props.coords) {
      this.fetchAddress()
    }
  }

  fetchAddress() {
    const { lat, lng } = this.props.coords
    getAddressFromCoords(lat, lng).then(address => {
      this.setState({ address })
    })
  }

  render() {
    return this.props.render(this.state)
  }
}

class App extends React.Component {
  render() {
    return (
      <div className="app">
        <GeoPosition render={(state) => (
          state.error ? (
            <div>Error: {state.error.message}</div>
          ) : state.coords ? (
            <GeoAddress coords={state.coords} render={({ error, address }) => (
              <Map
                lat={state.coords.lat}
                lng={state.coords.lng}
                info={error || address || "Loading..."}
              />
            )}/>
          ) : (
            <LoadingDots/>
          )
        )}/>
      </div>
    )
  }
}

Thoughts on Implementations

As Ryan states in the solution video, refactoring components to use a render prop is incredibly easy—almost literally just cut and paste and add a render method. This pattern makes it very easy to take some behavior and some state out of a component and make it reusable.

It's also very apparent that state is being composed together with this pattern. Since we are using callbacks, we get the power of closures, and this allows nested components using render prop callbacks to reference parent render prop callback arguments, thus allowing state to be composed dynamically in the render method, just like components.

The video for the solution differs slightly than the actual solution code here in that in the solution video getAddressFromCoords errors are actually caught and the <GeoAddress> error state is set. I also like the use of || in the render prop callback for <GeoAddress> to determine what text to render.

The solution video uses conditional checks inside of <GeoAddress> to only fetch the address if the coords props is available in order to handle the case where the fetch may be made before the map is fully loaded. While I understand that the check is saving us from when we pull lat and lng off of an undefined this.props.coords, I'm confused as to when this would ever happen. Since we only render <GeoAddress> if coords are available from the <GeoPosition> render prop callback argument, won't we always have the coords as a prop? Perhaps there is a case where somehow coords is not an object? Even if lat and lng are undefined, the Google Maps API is nice enough to throw a 400 error for invalid coordinates, which we would catch in the error handler callback. In any case, I suppose it is an easy way to avoid having our app blow up in case coords is not an object.

I like the solution videos use of componentDidUpdate life cycle hook to determine if the address needs to be updated, as opposed to my use of componentWillReceiveProps to perform the same check. The only reason being that componentDidUpdate will be able to use this.props.coords (just like componentDidMount) rather than nextProps.coords, which is what must be used with componentWillReceiveProps. I also like it because componentDidMount and componentDidUpdate seem like a better life cycle pair, and because I always forget how to spell componentWillReceiveProps ;)

"That's the whole point of this […]. Abstract behavior and state into a different component but still allow the app to decide what to render based on that state." — Ryan Florence

The above quote really struck a chord with me. Writing software all day, it's easy to get sucked into syntax and stick to comfortable and known patterns. But quotes like this one beg the question of what is reality? What is the code for? Using the render prop callback pattern is immensely beneficial at giving power back to the developer for how things should render.

"Good abstractions make it easy to add code and behavior; really good abstractions make it really easy to remove code." — Ryan Florence

Section 06 - Implementing React Router

This was the first section without a lecture, and was an exercise to use the patterns talked about throughout this course to reimplement React Router. Not only does it show off how React Router works on a basic level, it provides a practical application for the patterns discussed throughout this series.

Take Aways

We used context and we used compound components to pass around some implicit state (i.e. history) in order to make an imperative APIs more declarative (e.g. instead of everyone having to do history.push in their anchor tag click handlers, we can just declare a <Link> and wrap the behavior in the component's implementation that the rest of the app doesn't have to know about.)

We also used the render prop callback pattern to provide inline rendering for <Route> components. We also saw the use of a component prop that allows the developer to just pass the <Route> a component they would like to render. This pattern is very similar to the HOC pattern and the render prop callback pattern, and in the real React Router package is a way to pass on some props to the component provided.

Exercise Solution Code

// My solution

class Router extends React.Component {
  static childContextTypes = {
    history: PropTypes.object.isRequired
  }

  state = {
    location: null
  }

  getChildContext () {
    return {
      history: {
        push: (to) => this.history.push(to),
        location: this.state.location
      }
    }
  }

  componentDidMount () {
    this.history = createBrowserHistory()
    this.setState({ location: this.history.location })
    this.unsubscribe = this.history.listen(() => {
      this.setState({ location: this.history.location })
    })
  }

  componentWillUnmount () {
    this.unsubscribe()
  }

  render () {
    return this.props.children
  }
}

class Route extends React.Component {
  static contextTypes = {
    history: PropTypes.object.isRequired
  }

  render () {
    const { history } = this.context
    const { path, exact, render, component: Component } = this.props

    if (history.location) {
      if (exact && history.location.pathname !== path) {
        return null
      }

      if (!history.location.pathname.match(path)) {
        return null
      }
    }

    if (Component) {
      return <Component />
    }

    if (render) {
      return render()
    }

    return null
  }
}
// React Training's solution

class Router extends React.Component {
  history = createBrowserHistory()

  static childContextTypes = {
    history: this.history
  }

  getChildContext() {
    return {
      history: this.history
    }
  }

  componentDidMount() {
    this.unsubscribe = this.history.listen(() => {
      this.forceUpdate()
    })
  }

  componentWillUnmount() {
    this.unsubscribe()
  }

  render() {
    return this.props.children
  }
}

class Route extends React.Component {
  static contextTypes = {
    history: PropTypes.object.isRequired
  }

  render() {
    const { path, exact, render, component:Component } = this.props
    const { location } = this.context.history
    const match = exact
      ? location.pathname === path
      : location.pathname.startsWith(path)
    if (match) {
      if (render) {
        return render()
      } else if (Component) {
        return <Component/>
      } else {
        return null
      }
    } else {
      return null
    }
  }
}

Thoughts on Implementations

Figuring out how to check if the current location pathname matched the path prop was quite difficult

The snippet above ("My solution") is a trivial solution I came up with that includes code to meet the bare minimum necessary to make the UI behave as expected. However, having used React Router since version 4 was released, I knew there were other things that would be worth implementing to better mimic the real library. Most notably, I wanted to see if I could pass the correct props to components rendered within <Route>. There is an extra commit for my solution code that shows how I went about this extra step.

I had followed Tyler McGinnis's Build your own React Router v4 tutorial article some months ago, but it was obvious I had not retained everything I had learned from that article when I attempted to reimplement it again here. I inevitably succumbed to looking at my solution from the tutorial article for pointers on how to move forward, but still ended up creating a new solution on my own for this self-propagated exercise extension.

I continually forget that this.forceUpdate is a React-provided method, and therefore continually forget to think about it as a possible solution. Thinking of this would have come in handy when I was trying to figure out what to do with changes in the URL. I had settled on creating local component state to update the location context, which was unnecessary since the history instance property was already keeping track of URL changes.

I also forget about creating custom class instance properties when working with components, despite never hesitating to create state and custom class instance methods all the time. It's funny to me how complacent we can become with repetitive syntax and forget what is actually happening under the hood.

The solution video sets history as a class instance property on <Route>, which is great because it will be set before it would if it were in componentDidMount, which is necessary when using it on context since child consumer components will likely be expecting it to be available on their initial render. (This would have saved me from making the check to see if history.location was defined or not in the render method of <Route>.) This will be helpful to keep in mind when I develop with imperative APIs passed to children components.

I'm still learning new things that came out in ES6, like the startsWith string method that Ryan uses in the solution video to match the url location with the path prop in the <Route> component. Granted, the actual library needs to keep a reference of the match to pass down to the rendered component, and startsWith simply returns a boolean, but it's still good to know that this method exists on strings (as if there weren't already enough substring methods in JavaScript, heh).

Interesting that the React Training solution uses this.history for the value in childContextTypes—for one reason or another I had always assumed only PropTypes properties could be used here, but it makes total sense to use any object as a type. However, the solution video is clever enough to never open the browser developer tools during the recording, which would have shown an invalid type warning for the this.history being undefined on the <Router> component's initial render.

Section 07 - Implementing React Redux

React Redux is the binding to the Redux state management library for React. In this exercise-only section we roll our own <Provider> component and connect HOC, the two pillars of React Redux.

Take Aways

I have actually implemented a good deal of Redux on my own, writing functions like createStore and combineReducers by hand in order to understand how they work. However, I had never attempted to write parts of the React Redux library on my own before, so accomplishing that in this section's exercise was a lot of fun. Now all that's left to implement by hand is React itself ;)

It hit me that mapStateToProps and mapDispatchToProps are not required to connect React components to Redux, they are niceties that put the app developer in charge of what to call the state in the component. You could totally just pass the entire state to the connected component, but having mapStateToProps gives the power back to the developer to decide what slices of state a component should know about and also what to call them as props.

TL;DR

This exercise is more or less the heart of what React Redux is, using context and HOCs in order to facilitate a pleasant pattern to create apps in React.

Exercise Solution Code

// My solution

const connect = (mapStateToProps, mapDispatchToProps) => {
  return (Component) => {
    return class extends React.Component {
      static contextTypes = {
        store: PropTypes.object.isRequired
      }

      componentDidMount () {
        this.unsubscribe = this.context.store.subscribe(() => this.forceUpdate())
      }

      componentWillUnmount () {
        this.unsubscribe()
      }

      render () {
        const { getState, dispatch } = this.context.store
        const state = getState()
        const propsFromState = mapStateToProps(state)
        const propsFromDispatch = mapDispatchToProps(dispatch)

        return (
          <Component
            {...propsFromState}
            {...propsFromDispatch}
            {...this.props}
          />
        )
      }
    }
  }
}
// React Training's solution

const connect = (mapStateToProps, mapDispatchToProps) => {
  return (Component) => {
    return class extends React.Component {
      static contextTypes = {
        store: PropTypes.object.isRequired
      }

      componentDidMount() {
        this.unsubscribe = this.context.store.subscribe(() => {
          this.forceUpdate()
        })
      }

      componentWillUnmount() {
        this.unsubscribe()
      }

      render() {
        return (
          <Component
            {...mapStateToProps(this.context.store.getState())}
            {...mapDispatchToProps(this.context.store.dispatch)}
          />
        )
      }
    }
  }
}

Thoughts on Implementations

This is the first time that the React Training solution and mine were more or less exactly the same. The only notable difference between the two implementations are that spread this.props on the component returned by connect in order to preserve and prioritize any props that were added to <App> by the user. However, after playing around with manually passing a todos props to <App>, I am seeing now that React Redux does not prioritize nor preserve own props on a connected component if they happen to duplicate prop names passed in via mapStateToProps or mapDispatchToProps. So really my solution deviates more from the real library spec, and I should have passed this.props before passing the props from state and dispatch.

Today I learned that passing an uninvoked callback to a function will result in my app blowing up with errors if the function that invokes the callback is dependent on a specific receiver (i.e. the right this).

// `this` will be undefined when the callback is invoked, which will break the app
this.context.store.subscribe(this.forceUpdate)

// thanks to the callback closure, `this` is preserved as the component and the method works as expected
this.context.store.subscribe(() => this.forceUpdate())

Many a time I have done something like stuff.map(toSomethingElse) (as opposed to stuff.map((data) => toSomethingElse(data))) when I'm piping data from one function to the next, but I hadn't considered the implications this would have around this callback using this.

@ryanflorence
Copy link

Holy crap, this is awesome! Thank you so much for writing it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment