Title Text
Hello! I'm Jack
@Jack_Franklin
www.javascriptplayground.com
Please ask questions
This day is for you to learn React - consider me interruptible at any point - please ask at all times!
Slides + Talk
I'll show you some slides, maybe some code, and talk you through some concepts.
Exercise
We'll then stop and I'll ask you to do some exercises. You're going to be writing a lot of code today!
Set up
-
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
Components
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')
)
React + the DOM
- Tell React what each component should render.
- React takes care of the DOM for you.
ReactHub
Finding React based code on GitHub.
(Thanks to Richard Feldman!)
Running the workshop
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>
)
}
Dynamic content
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>
)
/part2
Components and components and components
<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>
)
/part3
Modules
We don't want every component in one file!
// hello.js
import React from 'react'
const Hello = props =>
<p>Hello, {props.name}</p>
export default Hello
//app.js
import Hello from './hello'
...
PropTypes
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
Arrays of Data
to
List of components
const repositories = [
{
id: 1,
name: 'jackfranklin/react-remote-data',
stars: 34,
},
{
id: 2,
name: 'ReactTraining/react-router',
stars: 25000,
},
]
<Repository ... />
<Repository ... />
and turn it into...
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
/part4
Stateful components
class App extends React.Component {
state = {
removedIds: [],
}
render() {
return (
<div className="content">
...
</div>
)
}
}
Change state => update UI
UI = representation of state at a given time
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 => ...)
}
/part5
{repositories
.filter(
repository =>
this.state.removedIds.indexOf(repository.id) === -1
)
.map(repository => ...)
}
hideRepository = id => {
this.setState(prevState => ({
...
}))
}
so-fetch-js
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)
Component lifecycle
https://reactjs.org/docs/react-component.html#the-component-lifecycle
componentDidMount
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
/part6
fetch(SEARCH_URL).then(result => {
// result.data.items is the array of repositories
this.setState(...)
})
Form elements
Controlled inputs
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,
})
}
/part7
Showing the "active" repository
Parent and child communication
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!
/part8
<Repository
repository={repository}
onRepositoryClick={this.onRepositoryClick}
/>
// inside Repository
<a href={...} onClick={this.onRepositoryClick} />
(hint: turn Repository into a class component!)
URLs!
https://reacttraining.com/react-router/
import {
BrowserRouter,
Route,
Link,
} from 'react-router-dom'
You wrap your app in a Router
Just add Routes!
<Route path="/foo" component={Foo} />
And Links!
<Link to="/foo">Go to Foo</Link>
/part9
Viewing a repository
/
Show the index page, search and results.
/repository/:id
Show the repository from the search results.
Changes
- Turn the "active repository" section into one that's only active on /repositories/:id
- Use <Link> components from React Router to allow the user to navigate
<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!)
When the user visits /repository/1
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"
Swap onRepositoryClick and just use <Link />
/part10
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"
A GitHub search engine
<App />
<SearchForm />
<SearchResults />
/
/results/:query
SearchForm
- Updates the form as the user types
- On submission, takes the user to /results/:query
this.props.history.push( `/results/${this.state.searchQuery}` )
SearchResults
- Takes the query from the URL param
- Fetches results and shows them.
Bugs!
- Nothing is shown on /results
- I'd like the SearchForm to be shown on the results page
- (There are more bugs to encounter...)
<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>
/part11
- Show a link back to the index page from /results with no query
- Use Switch to make SearchResults show on /results/:query
- Once on /results/:query, try searching for something else and see what happens...
Redux!
You don't need Redux a bunch of the time.
Dispatch actions
Update state
Get new state
A Redux store
A plain JS object that you can't edit or read from directly.
Reducer
Take an action and some state, produce the new state.
Counter
-
State: { count = 0 }
-
Dispatch action: { type: 'INCREMENT' }
-
Reducer gets (state, action)
-
Reducer updates state
-
store.getState(): { count = 1}
/part12
open your console!
react-redux
https://github.com/reactjs/react-redux
Redux can be used without React!
<Provider>
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.
By default components cannot read from the store
You have to connect them.
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.
/part13
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' })
Redux Dev Tools
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__()
)
Back to ReactHub!
Storing hiddenIds in Redux
case 'HIDE_REPOSITORY':
return {
...state,
hiddenIds: [...state.hiddenIds, action.id],
}
this.props.dispatch({
type: 'HIDE_REPOSITORY',
id: repository.id,
})
- Add reducer for adding an entry to state.hiddenIds
- Update the App component to use it to figure which repositories to render
- Dispatch your action when the user clicks a "X" by each entry.
- Add propTypes to the App component
/part14
Action creators
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: ...
Async and Redux
Redux Thunk
https://github.com/gaearon/redux-thunk
Allow action creators to dispatch other actions
Redux Middleware
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
/part15
export const fetchRepositories = () => {
return dispatch => {
return fetch(SEARCH_URL).then(result => {
dispatch(...)
})
}
}
Testing with Jest and Enzyme
Jest
Super cool test runner from Facebook. Quick, smart and very reliable.
Enzyme
A React testing library by AirBnB.
yarn run part16-test
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')
})
})
Part 16!
(there are no visuals!)
Testing user interaction
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
/part17
yarn run part17-test
Testing async components
fetch-mock
http://www.wheresrhys.co.uk/fetch-mock/
fetchMock.get('/repositories/', {
status: 200,
body: {
items: [{ id: 1, name: 'jack', stargazers_count: 22 }],
},
})
Waiting for async to complete
const nextTick = () => new Promise(resolve => setTimeout(resolve, 0))
await async
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)
})
/part18
yarn run part18-test
Building your own app
create-react-app
https://github.com/facebookincubator/create-react-app
yarn global add create-react-app npm install --global create-react-app
I'm hungry...
http://www.recipepuppy.com/about/api/
Recipe Puppy API!
http://www.recipepuppy.com/api/?i=onions,garlic&q=omelet
create-react-app recipe finder
Search for recipes
You may use Redux, React Router, or any other thing you're interested in :)
Search by ingredients
If there's anything you'd like to try that we haven't yet covered, now is a good time to ask!
ReactHub
By Jack Franklin
ReactHub
- 1,417