Day 2, Segment 1
Branch v2-12
Where does
The third way to create components with the
React will very probably keep
Arrow functions and
Today will have more fun things...
Data in React
Will create the details page.
// **** Details.js ****
import React from 'react'
const Details = React.createClass({
render () {
return (
<div className='details'>
<pre><code>
// just dump out the data
{JSON.stringify(this.props, null, 4)}
</code></pre>
</div>
)
}
})
export default Details
Add the new route to ClientApp.js
// **** ClientApp.js ****
import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter, Match } from 'react-router'
import Landing from './Landing'
import Search from './Search'
// 1. ++++
import Details from './Details'
import '../public/normalize.css'
import '../public/style.css'
const App = React.createClass({
render () {
return (
<BrowserRouter>
<div className='app'>
<Match exactly pattern='/' component={Landing} />
<Match
pattern='/search'
component={(props) => <Search shows={preload.shows} {...props} />}
/>
// 2. ++++
// :id → it's a parameter
<Match
pattern='/details/:id'
component={Details}
/>
</div>
</BrowserRouter>
)
}
})
render(<App />, document.getElementById('app'))
Make sure: <script src="/public/bundle.js"></script>
has the / in front.He sees in the details that
params.id
has a unique id that used for the url. The router also gives a lot of extra useful info (in the dump, see above). pathname
, location
, pattern
, location
.Using the shows JSON data in two places
In the workshop, it's just a json data file, but let's assume, that it's coming from the server, so you don't want to import it twice. We want to pass the data to a common ancestor component.
Let's refactor clientapp.js for this.
// **** ClientApp.js ****
import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter, Match } from 'react-router'
import Landing from './Landing'
import Search from './Search'
import Details from './Details'
// 1. ++++
import preload from '../public/data.json'
...
Stateless functional components
Details.js has no state, its stateless.So we could just do instead in details.js:
const Details = () => {
return <h1>hahahahah</h1>
}
export default Details
This still works. It's a stateless functional component. They're great and can be used everywhere. We can even pass down properties.
const Details = (props) => {
return <h1>{props.params.id}</h1>
}
export default Details
This just a render method. You don't do createClass. You still need to import React though, because we're using JSX and <h1>
is being transpiled to react.createElement...
16:10
We need to pass parameters to Search.js, but we just included it like so:
<Match pattern='/search' component={Search} />
Instead, we need to return it from anonymous function :
<Match
pattern='/search'
component={() => {
return <Search />
}}
/>
We need to pass properties to Search.
<Match
pattern='/search'
component={(props) => {
return <Search {...props} />
}}
/>
Then we need to pass the preload JSON data
<Match
pattern='/search'
component={(props) => {
return <Search shows={preload} {...props} />
}}
/>
How props get from one place to another?
We need to preserve the properties that Browserrouter uses. With
{...props}
we pass all the props properties. Where does this.props
come from?
In details.js:
{JSON.stringify(this.props, null, 4)}
The router passes down the properties. It's React Router that instantiates our component and passes down the properties.
Updates Search.js
import React from 'react'
import ShowCard from './ShowCard'
// 3. ++--- use the props instead of preload
// import preload from '../public/data.json'
const { arrayOf, shape, string } = React.PropTypes
const Search = React.createClass({
// 2. ++++ define the proptypes
propTypes: {
// array of elements
// shape: objects of title and description
shows: arrayOf(shape({
title: string,
description: string
}))
},
getInitialState () {...},
handleSearchTermChange (event) {...},
render () {
return (
<div className='search'>
<header>
...
</header>
<div>
// 1. ++-- use the props
// {preload.shows
{this.props.shows
.filter((show) => `${show.title} ${show.description}`.toUpperCase().indexOf(this.state.searchTerm.toUpperCase()) >= 0)
.map((show) => {
return (
<ShowCard key={show.imdbID} {...show} />
)
})}
</div>
</div>
)
}
})
export default Search
Everything still works, but now the shows data is coming from ClientApp instead of being imported from the JSON. arrayOf
propType
Above, for the Proptypes definition, you could also just use
array
, and not specify, the array
of what. You can also create your own proptypes.
Make the ClientApp pass the correct show down to Details
// **** ClientApp.js ****
import React from 'react'
import { render } from 'react-dom'
import { BrowserRouter, Match } from 'react-router'
import Landing from './Landing'
import Search from './Search'
// 1. ++++
import Details from './Details'
import preload from '../public/data.json'
import '../public/normalize.css'
import '../public/style.css'
const App = React.createClass({
render () {
return (
<BrowserRouter>
<div className='app'>
<Match exactly pattern='/' component={Landing} />
<Match
pattern='/search'
component={(props) => <Search shows={preload.shows} {...props} />}
/>
// 2. ++++
// pass down by the id
<Match
pattern='/details/:id'
component={Details}
/>
</div>
</BrowserRouter>
)
}
})
render(<App />, document.getElementById('app'))
Branch v2-14Proptypes declarations are only meant for documentation, and debugging (response to question).
Update clientapp details route
// **** ClientApp.js ****
// ...
<Match
pattern='/details/:id'
component={(props) => {
// filter the shows here
const shows = preload.shows.filter((show) => props.params.id === show.imdbID)
// then pass it to the component and
// return the component
return <Details show={shows[0]} {...props} />
}}
/>
// ...
The problem with the above code is that you assume that show id always exists.Alternative to React Router?
He doesn't really know one. React Router is the de facto standard routing library for React.
Now, the Details receives the show details in
this.props
.
// **** Details.js ****
<div className='details'>
<pre><code>
// this will show a property of "show"
// with show details
{JSON.stringify(this.props, null, 4)}
</code></pre>
</div>
React Router v4 guide and docs:
http://react-router.now.shThe v4 has a red icon, v3 is blue.
Component code ordering, top to bottom
- Declare the propTypes
getInitialState
functions- life-cycle methods
- my own functions I wrote (eg. handlers)
- the
render
function
Handling secure routes (authenticated routes)
He usually uses higher order components.
Higher Order Components
A component that encapsulates behaviour but does not render anything on its own. For example the BrowserRouter component. Does not output markup.
For more details watch Henrik's workshop...
Proptypes are almost like TDD
You identify first what you need to have, the do it. So, he starts writing new components with the proptypes.
Render the details with Details.js
So far it just dumped out the props...
// **** Details.js ****
import React from 'react'
const { shape, string } = React.PropTypes
const Details = React.createClass({
// 1. ++++ Define the proptypes
propTypes: {
show: shape({
title: string,
year: string,
poster: string,
trailer: string,
description: string
})
},
render () {
// 2. ++++ grab the data you need
const { title, description, year, poster, trailer } = this.props.show
return (
<div className='details'>
// 3. ++++ add the markup
<header>
<h1>svideo</h1>
</header>
<section>
<h1>{title}</h1>
<h2>({year})</h2>
<img src={`/public/img/posters/${poster}`} />
<p>{description}</p>
</section>
<div>
// 4. +++ also add the youtube video
// nocookie! :)
// frameBorder camelCase for React
<iframe src={`https://www.youtube-nocookie.com/embed/${trailer}?rel=0&controls=0&showinfo=0`} frameBorder='0' allowFullScreen />
</div>
</div>
)
}
})
export default Details
How to conditionally pass data to child components?
With anonymous functions in React router.
// **** ClientApp.js ****
// ...
<Match
pattern='/details/:id'
component={(props) => {
const shows = preload.shows.filter((show) => props.params.id === show.imdbID)
return <Details show={shows[0]} {...props} />
}}
/>
The component
can contain whatever logic (code, scripts) you need, such as conditionals (like above, with filter
).51:00
Move the header to a separate component since it's used in several places
Create Header.js
branch v2-15
// **** Header.js ****
import React from 'react'
import { Link } from 'react-router'
class Header extends React.Component {
render () {
return (
<header>
<h1>
<Link to='/'>
svideo
</Link>
</h1>
</header>
)
}
}
export default Header
The third way to create components with the class
syntax
class Header extends React.Component
Brian is not a big fan of the
This is how it's shown in the React docs. class
in ES6. (Kyle neither...). But React wants to move away from their own way of creating classes, and use the JavaScript standard way. You do lose some conveniences with this syntax though.Add this to Details.js
// **** Details.js ****
import React from 'react'
// 1. ++++
import Header from './Header'
const { shape, string } = React.PropTypes
const Details = React.createClass({
propTypes: {
show: shape({...})
},
render () {
const { title, description, year, poster, trailer } = this.props.show
return (
<div className='details'>
// 2. ++++
<Header />
<section>
...
Add a slightly advanced version of the header to search
We need to add a conditional logic to header. Not that straightforward with ES6 classes, since you cannot add properties to it.
You have to define the
Header.proptypes
outside the class
declaration.
// **** Header.js ****
class Header extends React.Component {
...
}
// 1. ++++
const { func, bool, string } = React.PropTypes
Header.propTypes = {
handleSearchTermChange: func,
showSearch: bool,
searchTerm: string
}
If you want to add your own functions to your component with the ES6 classes, you need to use the constructor
method
class Header extends React.Component {
constructor (props) {
super(props)
this.state = {
blah...
}
}
render () {
return (
...
)
}
}
Another annoyance is that ES6 classes don't auto-bind the context (this
) for you. So this will not work:
class Header extends React.Component {
constructor (props) {
super(props)
this.state = {
blah...
}
}
// 1. ++++ ad a method
someMethod () {
// 3. `this` context will not be correctly set!
this.setState({blah:'string'})
}
render () {
return (
// 2. ++++ call the method
<input onChange={this.someMethod()}>what</input>
)
}
}
So then, you need to do explicit binding:<input onChange={this.someMethod.bind(this)}>what</input>
But this is not performant. The component will be rendered thousands times in your app, and each time
bind
will run. So then, to get around this, you need hijack the methods:
class Header extends React.Component {
constructor (props) {
super(props)
this.state = {
}
// 1. ++++ define the bound methods
this.someMethod = this.someMethod.bind(this)
}
someMethod () {
this.setState({blah:'string'})
}
render () {
return (
<input onChange={this.someMethod()}>what</input>
)
}
}
This is very convoluted. This is why Brian prefers .createClass
. You don't have to worry about context. 1:05:00
Branch v2-17
Finish the Header.js conditional
// **** Header.js ****
import ...
class Header extends React.Component {
render () {
// 1. ++++
let utilSpace
// on search page:
if (this.props.showSearch) {
// move this from Search.js ↓
utilSpace = <input onChange={this.props.handleSearchTermChange} value={this.props.searchTerm} type='text' placeholder='Search' />
} else {
// on details page, go back to search:
utilSpace = (
<h2>
<Link to='/search'>
Back
</Link>
</h2>
)
}
return (
<header>
<h1>
<Link to='/'>
svideo
</Link>
</h1>
// 2. ++++ then render the
// utilSpace component
{utilSpace}
</header>
)
}
}
const { func, bool, string } = React.PropTypes
Header.propTypes = {
...
}
export default Header
Then use it on the search page
// **** Search.js ****
import React from 'react'
import ShowCard from './ShowCard'
// 1. ++++
import Header from './Header'
..
render () {
return (
<div className='search'>
// 2. +++++
<Header
// set showSearch to true, just by adding it
// no need to set it to true
showSearch
searchTerm={this.state.searchTerm}
// pass down the handler
// for the search term change
handleSearchTermChange={this.handleSearchTermChange}
/>
<div>
{this.props.shows
.filter((show) => `${show.title} ${show.description}`.toUpperCase().indexOf(this.state.searchTerm.toUpperCase()) >= 0)
.map((show) => {
return (
<ShowCard key={show.imdbID} {...show} />
)
})}
</div>
</div>
)
If you add a JSX attribute, it's default value will be true, just like with normal HTML attributes. Now, we've successfully extracted Header (search input) and Search into different components and they still talk to each other, by passing
handleSearchTermChange
as a props to the sub-component (Header). Add link to the details page
// **** Showcard.js ****
import React from 'react'
// 1. ++++
import { Link } from 'react-router'
const { string } = React.PropTypes
const ShowCard = React.createClass({
propTypes: {
...
// 4. ++++
imdbID: string.isRequired
},
render () {
// 2. ++-- imdbID
const { poster, title, year, description, imdbID } = this.props
return (
// 3. ++++ Add link
<Link to={`/details/${imdbID}`}>
<div className='show-card'>
...
</div>
</Link>
)
}
})
export default ShowCard
1:16:43He adds
.isRequired
to all of his proptypes. As you prefer...branch v2-1
Passing around functions between components
Explains passing the
handleSearchTermChange
. We pass down the function, but since it's a method of the parent component, this
is preserved (this.setState({..})
).Our app is almost finished. One more thing, and then refactor and Redux.
React will very probably keep createClass
People like it, use it (like Netflix) so they won't deprecate it. If ever they will, they will surely create a codemod that will convert your code to 90%.
1:26:00
Life cycle methods
Core concept in React.
getInitialState
So far we've been using
getInitialState
life cycle method. (or constructor
when using ES6 classes)componentWillMount
Called just before the component will be inserted into the DOM. He almost never uses it.
componentDidMount
Just after the component has been put into the DOM. More useful for him, because of universal, server-side rendering. componentWillMount is called in the NodeJS environment, while componentDidMount is not called in NodeJS env.
Of course, in React you rarely want to interact with the DOM directly, but...
- You want to do AJAX at this stage.
- interact with jQuery (if you have to)
- add side listeners (if you have to)
- D3
componentWillUnmount
Component is leaving the DOM. For cleanup. Remove event listeners. Unsubscribe from events.
getDefaultProps
Less used. Usually called at the root component. Usually used for dependency injection (Brian not a fan of dependency injection). Brian in fact never used this.
shouldComponentUpdate
Used for performance reasons. Calculating what has to re-render on deeply nested objects is very expensive. For example, here you can check if some change in the data should really trigger a component update or not.
Or, if you want your component to never update, then you can call
return
on it (or return false
not clear on the video). 1:35:20Try to avoid messing with it, unless absolutely necessary. A. React is already very performance oriented, B. It can create maintainability issues later (you don't know why things don't update even if the data has changed).
React perf tools
Visualise where your application spends the most time.
- print inclusive → includes life-cycle methods
- print exclusive → does not include life-cycle methods
- print waisted → where repaints are waisted
shouldComponentUpdate
. Performance tips
- avoid caching, memorisation (complicates code)
- avoid inline SVGs. React can do it, but they're slow.
Question about animation. Not clear the answer...
Most life-cycle methods are only useful for interacting D3...
AJAX request with Axios
Branch v2-17
// **** Details.js ****
import React from 'react'
// 1. ++++ axios AJAX library
import axios from 'axios'
import Header from './Header'
const { shape, string } = React.PropTypes
const Details = React.createClass({
propTypes: {
show: shape({
...
// 4. ++++
imdbID: string
})
},
// 2. ++++
// for now just an empty object
getInitialState () {
return {
omdbData: {}
}
},
// 3. ++++
componentDidMount () {
// do the ajax
// omdb is an open-source api for imdb
axios.get(`http://www.omdbapi.com/?i=${this.props.show.imdbID}`)
.then((response) => {
this.setState({omdbData: response.data})
})
// handle the error
.catch((error) => console.error('axios error', error))
},
render () {
const { title, description, year, poster, trailer } = this.props.show
// 4. ++++
// uses rating to detect if the
// data came back
let rating
if (this.state.omdbData.imdbRating) {
rating = <h3>{this.state.omdbData.imdbRating}</h3>
} else {
// put up the spinner
rating = <img src='/public/img/loading.png' alt='loading indicator' />
}
return (
<div className='details'>
<Header />
<section>
<h1>{title}</h1>
<h2>({year})</h2>
{rating}
<img src={`/public/img/posters/${poster}`} />
<p>{description}</p>
</section>
<div>
<iframe src={`https://www.youtube-nocookie.com/embed/${trailer}?rel=0&controls=0&showinfo=0`} frameBorder='0' allowFullScreen />
</div>
</div>
)
}
})
export default Details
1:47:00componentDidMount will request the data when navigating out and back, that we will fix with Redux.
It works! And pretty fast! Has to throttle the network speed in Firefor responsive mode, to see the preloader before the response comes back (and fires
this.setState({omdbData: response.data})
that will fire the rerender.Arrow functions and this
binding
Snippet:
axios.get(`http://www.omdbapi.com/?i=${this.props.show.imdbID}`)
.then((response) => {
this.setState({omdbData: response.data})
})
If above, you would be using regular function
declaration, instead of =>
, then the this
would be bound to the wrong context (probably the window). Arrow functions don't create new context, but use the same context as it was called in, in our case component (we're inside
componentDidMount
method).The app is finished now. We will continue with devtools, Redux.