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.
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.
"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.
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.
// 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
}
}
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.
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.
"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.
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
// 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>
);
}
}
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.
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.
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.
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>
)
}
}
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!).
- 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
- I liked setting
currentTime
back to zero inonEnded
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
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.
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.
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.
// 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}
/>
)
}
}
)
"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 avalue
prop as well as anonChange
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 usingComponent
as a named export ofReact
- I didn't consider replacing
set
fromlocal-storage
with a setter likesetStorage
passed in from our HOC, which removes forcing users to import and acknowledge the API ofset
- 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 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.
"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.
"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
.
// 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>
)
}
}
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
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.
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.
// 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
}
}
}
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.
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.
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.
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.
// 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)}
/>
)
}
}
}
}
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
.
Holy crap, this is awesome! Thank you so much for writing it!