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

  1. State: { count = 0 }
  2. Dispatch action: { type: 'INCREMENT' }
  3. Reducer gets (state, action)
  4. Reducer updates state
  5. 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

  • 338

More from Jack Franklin