@Jack_Franklin
www.javascriptplayground.com
This day is for you to learn React - consider me interruptible at any point - please ask at all times!
I'll show you some slides, maybe some code, and talk you through some concepts.
We'll then stop and I'll ask you to do some exercises. You're going to be writing a lot of code today!
Node version 8 (node --version)
npm version 5 or Yarn version 1 (npm --version, yarn --version)
https://github.com/jackfranklin/react-hub cloned locally
{yarn, npm} install
{yarn, npm} start
An editor of your choice with the React plugin installed.
Join: https://tlk.io/jack-react
const HelloWorld = function() {
return <p>Hello World</p>
}
// or
const HelloWorld = () => {
return <p>Hello World</p>
}
// or
const HelloWorld = () => <p>Hello World</p>
JSX
import React from 'react'
import { render } from 'react-dom'
const HelloWorld = () => <p>Hello World</p>
render(
<HelloWorld />,
document.getElementById('react-root')
)
Finding React based code on GitHub.
(Thanks to Richard Feldman!)
npm start
yarn start
/part1
const App = () => {
return (
<div className="content">
<header>
ReactHub!
{/* EXERCISES!
* 1. Give this `span` some text, such as: "GitHub, for React things"
* 2. Give the `span` a class of "tagline"
* 3. Extract a `<Tagline />` React component that renders the span.
*/}
<span />
</header>
</div>
)
}
const name = "Jack"
const App = () => (
<div>
<p>Hello, my name is {name}</p>
</div>
)
const person = {
name: 'Jack',
}
const App = () => (
<div>
<p>Hello, my name is {person.name}</p>
</div>
)
const person = {
firstName: 'Jack',
lastName: 'Franklin',
}
const App = () => (
<div>
<p>
Hello, {person.firstName} {person.lastName}
</p>
</div>
)
<li>
<span className="star-count">
{sampleRepository.stars}
</span>
<a href={`https://github.com/${sampleRepository.name}`}>
{sampleRepository.name}
</a>
</li>
What if we had multiple repositories?
Or wanted to list them elsewhere on the site?
const Hello = () => <p>Hello, Jack</p>
const App = () => (
<div>
<Hello />
</div>
)
const Hello = props =>
<p>Hello, {props.name}</p>
const App = () => (
<div>
<Hello name="Jack" />
</div>
)
// hello.js
import React from 'react'
const Hello = props =>
<p>Hello, {props.name}</p>
export default Hello
//app.js
import Hello from './hello'
...
const Hello = props =>
<p>Hello, { props.name }</p>
Hello.propTypes = {
...
}
What props this component takes, what the types are, and if they are required or not.
Future you (or your team) will be glad that you took the time to do this!
const Hello = props =>
<p>Hello, { props.name }</p>
Hello.propTypes = {
name: PropTypes.string.isRequired,
}
https://reactjs.org/docs/typechecking-with-proptypes.html
to
const repositories = [
{
id: 1,
name: 'jackfranklin/react-remote-data',
stars: 34,
},
{
id: 2,
name: 'ReactTraining/react-router',
stars: 25000,
},
]
<Repository ... />
<Repository ... />
const numbers = [1, 2, 3]
const doubled = numbers.map(x => x * 2)
// => [2, 4, 6]
const repositories = [...]
repositories.map(repo =>
turnRepoIntoReactComponent(repo)
)
const repositories = [...]
repositories.map(repo =>
<li>{repo.name}</li>
)
const repositories = [...]
repositories.map(repo =>
<li key={repo.id}>{repo.name}</li>
)
key prop = something unique to each item in the array
class App extends React.Component {
state = {
removedIds: [],
}
render() {
return (
<div className="content">
...
</div>
)
}
}
class App extends React.Component {
render() {
return (
<div className="content">
<header>
<h1>ReactHub!</h1>
<span className="tagline">GitHub, for React things</span>
</header>
<ul className="results">
{repositories.map(repository => (
<li key={repository.id}>
<Repository repository={repository} />
<button
className="removeBtn"
onClick={() => ...}
>
X
</button>
</li>
))}
</ul>
</div>
)
}
}
<button className="removeBtn"
onClick={() =>
this.hideRepository(repository.id)
}
>X</button>
class App extends React.Component {
state = {
removedIds: [],
}
hideRepository = id => {
this.setState(prevState => ({}))
}
this.setState(prevState => ({}))
this.setState(function(prevState) {
return {
// new state goes here
}
})
The first form of this.setState - when you need the previous state.
this.setState({
...new state goes here...
})
Second form of this.setState - when the previous state doesn't matter
hideRepository = id => {
this.setState(prevState => ({
removedIds: prevState.removedIds.concat([id]),
}))
}
hideRepository = id => {
this.setState(prevState => ({
removedIds: [...prevState.removedIds, id],
}))
}
{repositories
.filter(
repository =>
this.state.removedIds.indexOf(repository.id) === -1
)
.map(repository => ...)
}
{repositories
.filter(
repository =>
this.state.removedIds.indexOf(repository.id) === -1
)
.map(repository => ...)
}
hideRepository = id => {
this.setState(prevState => ({
...
}))
}
const SEARCH_URL = `
http://github-proxy-api.herokuapp.com/search/repositories?q=react+language:javascript+fork:false+stars:>=1000
`
github-proxy-api.herokuapp.com
{
items: [{ id: 1, stargazers_count: 200, ... }, ...]
}
(stars => stargazers_count)
https://reactjs.org/docs/react-component.html#the-component-lifecycle
https://reactjs.org/docs/react-component.html#componentdidmount
If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
class App extends React.Component {
state = {
repositories: [],
isLoading: true,
removedIds: [],
}
...
}
{this.state.isLoading && <div className="loader">Loading...</div>}
Conditional rendering in JSX
fetch(SEARCH_URL).then(result => {
// result.data.items is the array of repositories
this.setState(...)
})
React controls the value of an input
And React controls the onChange event of an input.
<form onSubmit={this.searchGithub}>
<input
type="text"
className="search-query"
placeholder="react"
value={this.state.searchQuery}
onChange={this.updateSearchQuery}
/>
<button type="submit" className="search-button">
Search
</button>
</form>
class App extends React.Component {
state = {
repositories: [],
isLoading: true,
removedIds: [],
searchQuery: 'react',
}
...
}
updateSearchQuery = event => {
this.setState({
searchQuery: event.target.value,
})
}
Sometimes we'll have state in the parent
that we give to the child
<SomeChildComp foo={this.state.foo} />
And sometimes the child needs to let us know that the state has changed.
<SomeChildComp
foo={this.state.foo}
onFooChange={this.onFooChange}
/>
Parent
Child
foo=this.state.foo
hey parent, foo changed!
class App extends Component {
state = {
activeRepository: -1,
}
}
<Repository
repository={repository}
onRepositoryClick={this.onRepositoryClick}
/>
this.state.repositories.find(
repo => repo.id === this.state.activeRepository
)
Hint for getting the right repository!
<Repository
repository={repository}
onRepositoryClick={this.onRepositoryClick}
/>
// inside Repository
<a href={...} onClick={this.onRepositoryClick} />
(hint: turn Repository into a class component!)
https://reacttraining.com/react-router/
import {
BrowserRouter,
Route,
Link,
} from 'react-router-dom'
<Route path="/foo" component={Foo} />
<Link to="/foo">Go to Foo</Link>
Show the index page, search and results.
Show the repository from the search results.
<Route render=... />
// if I visit /repository/2
<Route
path="/repository/:id"
render={props => {
return <p>This route matched!</p>
}}
/>
props.match.params.id === "2"
(note that all params are strings!)
1. Check that we have any repositories.
2. Search for a repository with a matching ID
3. If we find it, render the <Repository /> component
4. If we don't, show "Repository not found"
1. Check that we have any repositories.
2. Search for a repository with a matching ID
3. If we find it, render the <Repository /> component
4. If we don't, show "Repository not found"
<App />
<SearchForm />
<SearchResults />
/
/results/:query
this.props.history.push( `/results/${this.state.searchQuery}` )
<Switch>
<Route path="/foo" component={TestingComponent} />
<Route path="/foo/bar" component={TestingComponent} />
<Route path="/foo/baz" component={TestingComponent} />
</Switch>
Only one of these will ever be rendered.
Switch renders the first one that matches.
<Switch>
<Route path="/" component={TestingComponent} />
<Route path="/foo" component={TestingComponent} />
</Switch>
A plain JS object that you can't edit or read from directly.
Take an action and some state, produce the new state.
State: { count = 0 }
Dispatch action: { type: 'INCREMENT' }
Reducer gets (state, action)
Reducer updates state
store.getState(): { count = 1}
open your console!
https://github.com/reactjs/react-redux
const store = createStore(reducer);
import { Provider } from 'react-redux'
render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('react-root'))
Allows your components to access the store.
import { connect } from 'react-redux'
class App extends Component {...}
const ConnectedApp = connect(mapReduxStateToProps)(App)
export default ConnectedApp
(regular component)
mapReduxStateToProps
const mapReduxStateToProps = reduxState => ({
count: reduxState.count,
})
(regular component)
mapReduxStateToProps
Controls what parts of the store a component is allowed to read and access.
this.props.dispatch({ type: 'INCREMENT' })
A connected component can also dispatch actions.
import { connect } from 'react-redux'
class App extends Component {...}
const mapReduxStateToProps = state => ({
count: state.count
})
const ConnectedApp = connect(mapReduxStateToProps)(App)
export default ConnectedApp
this.props.dispatch({ type: 'INCREMENT' })
https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd
https://addons.mozilla.org/en-US/firefox/addon/remotedev/
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
case 'HIDE_REPOSITORY':
return {
...state,
hiddenIds: [...state.hiddenIds, action.id],
}
this.props.dispatch({
type: 'HIDE_REPOSITORY',
id: repository.id,
})
this.props.dispatch({ type: 'HIDE_REPOSITORY', id: id })
import { hideRepository } from './actions'
this.props.dispatch(hideRepository(id))
export const HIDE_REPOSITORY = 'HIDE_REPOSITORY'
export const hideRepository = id => ({
type: HIDE_REPOSITORY,
id,
})
// in reducer
switch (action.type)
case HIDE_REPOSITORY: ...
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunk from 'redux-thunk'
// create reducer here
const store = createStore(reducer,
composeWithDevTools(applyMiddleware(thunk))
)
export const fetchRepositories = () => {
return dispatch => {
}
}
a "thunk" is just an action creator that returns a function that will get called with dispatch
this means we can do some async work and then dispatch another action
export const fetchRepositories = () => {
return dispatch => {
return fetch(SEARCH_URL).then(result => {
dispatch(...)
})
}
}
so we could make a network request, and then dispatch another action with the data once we have it
export const fetchRepositories = () => {
return dispatch => {
return fetch(SEARCH_URL).then(result => {
dispatch(...)
})
}
}
Super cool test runner from Facebook. Quick, smart and very reliable.
A React testing library by AirBnB.
shallow rendering lets you render a component “one level deep” and assert facts about what its render method returns, without worrying about the behavior of child components, which are not instantiated or rendered.
https://reactjs.org/docs/shallow-renderer.html
import React from 'react'
import { shallow } from 'enzyme'
import Repository from './repository'
describe('Part 16 tests', () => {
it('renders the right number of stars', () => {
const repository = {
stars: 33,
name: 'test',
id: 1,
}
const wrapper = shallow(<Repository repository={repository} />)
expect(wrapper.find('.star-count').text()).toEqual('33')
})
})
(there are no visuals!)
it('lists some repositories', () => {
const wrapper = shallow(<App />)
expect(wrapper.find('Repository').length).toEqual(3)
})
Testing the <App /> component
it('can click a button to remove a repository', () => {
const wrapper = shallow(<App />)
const button = wrapper.find('li button').first()
button.simulate('click')
expect(wrapper.find('Repository').length).toEqual(2)
})
this assertion isn't that strong...
did it remove the right item?
expect(
wrapper.find('Repository').map(repo => repo.props().repository.name)
).toEqual(['ReactTraining/react-router', 'facebook/react'])
Enzyme lets us map over search results to read their props
it('can click the reset button to unhide all repositories', () => {
const wrapper = shallow(<App />)
const hideBtn = wrapper.find('li button').first()
hideBtn.simulate('click')
expect(wrapper.find('Repository').length).toEqual(2)
const resetBtn = wrapper.find('button.reset')
resetBtn.simulate('click')
expect(wrapper.find('Repository').length).toEqual(3)
})
can you get this test working?
● Part 17 App › can click the reset button to unhide all repositories
Method “simulate” is only meant to be run on a single node. 0 found instead.
step 1: create a reset button
yarn run part17-test
fetchMock.get('/repositories/', {
status: 200,
body: {
items: [{ id: 1, name: 'jack', stargazers_count: 22 }],
},
})
const nextTick = () => new Promise(resolve => setTimeout(resolve, 0))
it('tests something', async () => {
// your async thing hasn't finished yet so you can't
// assert on it yet
await nextTick()
// now it's finished so you can!
})
it('lists some repositories', async () => {
fetchMock.get(SEARCH_URL, {
status: 200,
body: {
items: [{ id: 1, name: 'jack', stargazers_count: 22 }],
},
})
const wrapper = shallow(<App />)
await nextTick()
wrapper.update()
expect(wrapper.find('Repository').length).toEqual(1)
})
https://github.com/facebookincubator/create-react-app
yarn global add create-react-app npm install --global create-react-app
http://www.recipepuppy.com/about/api/
Recipe Puppy API!
http://www.recipepuppy.com/api/?i=onions,garlic&q=omelet
You may use Redux, React Router, or any other thing you're interested in :)
If there's anything you'd like to try that we haven't yet covered, now is a good time to ask!