render2018
@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.
I will share the slides at the end of the workshop :)
We'll then stop and I'll ask you to do some exercises. You're going to be writing a lot of code today!
You won't pick everything up right away, no one does! Please ask lots of questions :)
Node version 8 (node --version)
npm version 5 or Yarn version 1 (npm --version, yarn --version)
https://github.com/jackfranklin/react-fundamentals-workshop cloned locally
{yarn, npm} install
An editor of your choice with the React plugin installed.
Join: https://tlk.io/jack-react
*ok there's one but I wrote it so that's allowed
npm run exercise 1
yarn run exercise 2
yarn run exercise 1
npm run exercise 1
import '../common.css'
import ReactDOM from 'react-dom'
import React from 'react'
const HelloWorld = () => {
// TODO: can you change the h1 to another element?
// how would we give the h1 a class name?
return React.createElement('h1', null, 'Hello World')
}
ReactDOM.render(
React.createElement(HelloWorld),
document.getElementById('react-root')
)
don't worry about this bit
import '../common.css'
import ReactDOM from 'react-dom'
import React from 'react'
const HelloWorld = () => {
// TODO: can you change the h1 to another element?
// how would we give the h1 a class name?
return React.createElement('h1', null, 'Hello World')
}
ReactDOM.render(
React.createElement(HelloWorld),
document.getElementById('react-root')
)
import React and ReactDOM
import '../common.css'
import ReactDOM from 'react-dom'
import React from 'react'
const HelloWorld = () => {
// TODO: can you change the h1 to another element?
// how would we give the h1 a class name?
return React.createElement('h1', null, 'Hello World')
}
ReactDOM.render(
React.createElement(HelloWorld),
document.getElementById('react-root')
)
our first component!
import '../common.css'
import ReactDOM from 'react-dom'
import React from 'react'
const HelloWorld = () => {
// TODO: can you change the h1 to another element?
// how would we give the h1 a class name?
return React.createElement('h1', null, 'Hello World')
}
ReactDOM.render(
React.createElement(HelloWorld),
document.getElementById('react-root')
)
our first component!
Exercise 1
render our first component into the DOM
const HelloWorld = () => {
// TODO: can you change the h1 to another element?
// how would we give the h1 a class name?
return <h1>Hello World</h1>
}
ReactDOM.render(<HelloWorld />,
document.getElementById('react-root')
)
JSX!
remember, JSX gets compiled to React.createElement.
It's just JavaScript!
Exercise 2
const HelloWorld = props => {
// TODO: pass through another prop to customise the greeting
// rather than it always be hardcoded as Hello
return <h1>Hello, {props.name}</h1>
}
ReactDOM.render(
<HelloWorld name="Jack" />,
document.getElementById('react-root')
)
we can pass properties (or, props) through to components to configure them or change their behaviour
Exercise 3
remember, props are all just JavaScript, so you can pass through any data type - these aren't HTML attributes
const bunchOfProps = {
name: 'Jack',
age: 25,
colour: 'blue',
}
ReactDOM.render(
<HelloWorld
name={bunchOfProps.name}
age={bunchOfProps.age}
colour={bunchOfProps.colour}
/>,
document.getElementById('react-root')
)
Exercise 4
ReactDOM.render(
<HelloWorld {...bunchOfProps} />,
document.getElementById('react-root')
)
or:
HelloWorld.propTypes = {
name: PropTypes.string.isRequired,
colour: PropTypes.oneOf(['blue', 'red']).isRequired,
}
Exercise 5
Documenting your components with prop types
This seems like a chore, but trust me, you'll thank yourself in the future!
https://reactjs.org/docs/typechecking-with-proptypes.html
const AskQuestion = () => {
return <p>How is your day going today?</p>
}
const HelloWorld = props => {
return (
<div>
<AskQuestion />
<h1>
{props.greeting}, {props.name}
</h1>
</div>
)
}
Exercise 6
Components can render other components.
React components must start with a capital letter.
Data a component is given and uses but cannot change.
Data a component owns and can change.
What we've used so far. These components cannot have state.
We define our components as classes. These components can have state.
import React, { Component } from 'react'
class MyComponent extends Component {
constructor(props) {
super(props)
this.state = {...}
}
render() {
return <p>Hello world</p>
}
}
this is boilerplate you don't need to worry about
this is like the body of the functional components we have
been using so far!
onButtonClickIncrement() {
}
render() {
return (
<div>
<p>current count: {this.state.count}</p>
<button onClick={this.onButtonClickIncrement.bind(this)}>
Click to increment
</button>
</div>
)
}
we have to bind to ensure the right scope within the event handler
(we'll see a nicer way to do this later on)
this.setState({
newValue: 3,
})
when the new state doesn't depend on the old state
this.setState(function(previousState) {
return {
newValue: previousState.newValue + 1
}
})
when the new state does depend on the old state
class Counter extends Component {
constructor(props) {
super(props)
this.state = {
count: 0,
}
}
onButtonClickIncrement() {
this.setState(prevState => {
return { count: prevState.count + 1 }
})
}
render() {
return (
<div>
<p>current count: {this.state.count}</p>
<button onClick={this.onButtonClickIncrement.bind(this)}>
Click to increment
</button>
</div>
)
}
}
Exercise 7
const Parent = () => {
return <Child />
}
<div>
<Count count={this.state.count} />
<button onClick={this.onButtonClickIncrement.bind(this)}>
Click to increment
</button>
</div>
Exercise 8
The Count component gets a count property, that has come from state of its parent.
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!
Exercise 9
<Count
count={this.state.count1}
onIncrement={this.incrementCount1.bind(this)}
/>
here's some data for you to render
and when that data changes, this is how you tell me about it
this.state = {
counts: [0, 0, 0],
}
often we have lists of data that we want to render
return (
<div>
<Count
count={this.state.counts[0]}
onIncrement={this.incrementCount.bind(this, 0)}
/>
<Count
count={this.state.counts[1]}
onIncrement={this.incrementCount.bind(this, 1)}
/>
</div>
)
and this is a pretty manual way of doing it
Exercise 10
const numbers = [1, 2, 3]
const newNumbers = numbers.map(function(x) {
return x * 2
})
// or
const newNumbers = numbers.map(x => x * 2)
newNumbers === [2, 4, 6]
mapping over arrays in JavaScript
Exercise 10
so could we take our array of counts, and map them into an array of <Count /> components?
this.state = {
counts: [0, 0, 0],
}
this.state.counts.map((count, index) => {
return (
<Count
count={count}
onIncrement={this.incrementCount.bind(this, index)
/>
)
})
Exercise 11
React Children
<Count count={this.state.count} />
<Count count={this.state.count}>
<p>some random child</p>
</Count>
this.props.children
<Count
count={this.state.count}
children={<p>some random child</p>}
/>
remember, props can be anything!
we'll see more usages of React's children later on :)
Time for a quick break! Any questions?
This workshop purposefully avoids talking about tooling and set up. We're focusing purely on React today (but questions are welcome!)
But I want to take a few minutes to quickly talk about some of the things going on in the workshop behind the scenes.
A tool that takes all of our code and generates a bundle of HTML, CSS and JS for us.
Today I'm using https://parceljs.org/, but I'm also a big fan of https://webpack.js.org/
A tool that takes our modern JS and converts it back into JS that older browsers can use. Depending on what browsers you support, these will do different transforms.
Today we're using https://babeljs.io/ with some plugins:
- babel-preset-env
- babel-preset-react
Enables https://github.com/tc39/proposal-object-rest-spread, which is at Stage 4, meaning it will become part of JavaScript.
Enables https://github.com/tc39/proposal-class-fields, which is a Stage 3 proposal.
onButtonClickIncrement() {
}
<button onClick={this.onButtonClickIncrement.bind(this)}>
onButtonClickIncrement = () => {
}
<button onClick={this.onButtonClickIncrement}>
becomes...
arrow functions are always bound to the scope of the thing that defined them
Foo.propTypes = {...}
constructor(props) {
super(props)
this.state = {...}
}
class Foo extends Component {
static propTypes = {...}
state = {...}
}
becomes...
I find formatting code rather mundane and boring - so I let tools do it for me!
https://prettier.io/ is fantastic and there are editor plugins available for all popular editors.
import fetch from 'so-fetch-js'
fetch('/users').then(response => {
console.log(response.data)
})
https://jsonplaceholder.typicode.com/posts
[
{
userId: 1,
id: 1,
title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
body: "quia et suscipit suscipit recusandae consequuntur expedita et cum reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto"
},
...
]
https://jsonplaceholder.typicode.com/posts/1
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.
Exercise 12
componentDidMount() {
const urlForPost = `https://jsonplaceholder.typicode.com/posts/${
this.props.id
}`
fetch(urlForPost).then(response => {
const post = response.data
// TODO: put this post into the state
console.log('I got the post!', post)
})
}
{this.state.post === null && <div>Loading...</div>}
Conditional rendering in JSX
render() {
return this.state.post ? (
<div>
<h1>{this.state.post.title}</h1>
<p>{this.state.post.body}</p>
</div>
) : (
<p>Loading</p>
)
}
React controls the value of an input
And React controls the onChange event of an input.
<input
type="text"
name="post-id"
value={this.state.userPostInput}
onChange={this.userInputChange}
/>
<form onSubmit={this.onSubmit}>
value: the actual value of the input box
onChange: what React calls when the user types in the box, so we can update the value
userInputChange = event => {
this.setState({
userPostInput: event.target.value,
})
}
event.target.value === the latest value from the input that the user has typed
Exercise 13 (unlucky for some)
<form onSubmit={this.onSubmit} className="search-form">
<label>
Please enter the ID of a post
<input
type="text"
name="post-id"
value={this.state.userPostInput}
onChange={this.userInputChange}
/>
</label>
<button type="submit">Go</button>
{/* TODO: add another button that clears out the user input value */}
</form>
{this.state.searchId && (
<p>The ID you searched for is: {this.state.searchId}</p>
)}
userInputChange = e => {
console.log('got user input value', e.target.value)
// TODO: update the userPostInput state with the new value when the user types
}
onSubmit = e => {
e.preventDefault()
console.log('got form submit!')
// TODO: update the searchID state with the latest user post ID when the form is submitted
}
Exercise 14
onSubmit = e => {
e.preventDefault()
console.log('got form submit!')
// TODO: call this.fetchPost(), passing in the right ID
}
fetching based off user input
export default class Post extends Component {
...
}
import Post from './post'
https://reactjs.org/docs/react-component.html#componentdidupdate
Exercise 15
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
Use this as an opportunity to operate on the DOM when the component has been updated. This is also a good place to do network requests as long as you compare the current props to previous props (e.g. a network request may not be necessary if the props have not changed).
Exercise 15
componentDidUpdate(prevProps, prevState) {
}
don't forget to check that the props or state have changed!
Exercise 15
componentDidUpdate(prevProps, prevState) {
if (prevProps.id !== this.props.id) {
}
}
if this check passes, we can do our work
Exercise 15
Exercise 15
PostSearch: the main component that lets a user search by post ID
Post: the component that takes a post ID, fetches it, then renders it.
<form onSubmit={this.onSubmit} className="search-form">
<label>
Please enter the ID of a post
<input
type="text"
name="post-id"
value={this.state.userPostInput}
onChange={this.userInputChange}
/>
</label>
<button type="submit">Go</button>
</form>
<Post id={this.state.searchId} />
Exercise 15
But there's a bug!
Filling in a new ID and hitting 'go' does not load the new post.
export default class Post extends Component {
static propTypes = {
id: PropTypes.number,
}
state = {
post: null,
}
componentDidMount() {
this.fetchPost()
}
...
}
Exercise 16
we've pulled out a component for getting the user's search ID, but it's not quite done yet.
class PostSearch extends Component {
state = {
searchId: 1,
}
onSubmit = id => {
this.setState({ searchId: id })
}
render() {
return (
<div>
<UserInput onSearchInputChange={this.onSubmit} />
<Post id={this.state.searchId} />
</div>
)
}
}
this is more parent to child
communication like we saw earlier
Any questions? :)
<div>
<UserInput onSearchInputChange={this.onSubmit} />
<Post id={this.state.searchId} />
</div>
this is our render function, but we have no control over how it's rendering!
<Post /> contains logic about fetching a post that we don't want to care about.
But we want to have control over what to render.
https://reactjs.org/docs/react-api.html#reactchildrenmap
React provides an API to take all the children given to a component and edit them.
// imagine this.props.children are all <p>foo</p>
return React.Children.map(this.props.children, child => {
// we can edit the child and return a new one
})
https://reactjs.org/docs/react-api.html#reactchildrenmap
React also gives us cloneElement to take copies of the child but give it extra properties
// imagine this.props.children are all <p>foo</p>
return React.Children.map(this.props.children, child => {
return React.cloneElement(child, { className: 'foo'})
})
so in our case, the Post component can...
const PostOutput = props =>
props.post ? (
<div>
<span>Loaded post ID: {props.post.id}</span>
<h1>{props.post.title}</h1>
<p>{props.post.body}</p>
</div>
) : (
<p>Loading</p>
)
<Post id={this.state.searchId}>
<PostOutput />
</Post>
return React.Children.map(...)
Exercise 17
const createAdder = x => {
return function(y) { return x + y };
}
const createAdder = x => y => x + y
const addTwo = createAdder(2)
addTwo(2) // 4
const wrapWithDiv = Component => {
const NewComponent = props => (
return <div><Component {...props}</div>
)
return NewComponent
}
const Hello = () => <p>Hello world!</p>
const WrappedComponent = wrapWithDiv(Hello)
<WrappedComponent />
functions that return React components
Exercise 18
const withPost = ChildComponent => {
return class Post extends Component {
static propTypes = {
id: PropTypes.number,
}
state = {
post: null,
}
...
render() {
return <ChildComponent {...this.props} post={this.state.post} />
}
}
}
export default withPost
takes a ChildComponent as an argument
we return a component that renders our child component with some extra props
import withPost from './post'
export default class Post extends Component {
static propTypes = {
id: PropTypes.number,
render: PropTypes.func.isRequired,
}
...
render() {
return this.props.render(this.state.post)
}
}
takes a render prop that we call to render
<Post
id={this.state.searchId}
render={post =>
post ? (
<div>
<span>Loaded post ID: {post.id}</span>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
) : (
<p>Loading</p>
)
}
/>
Exercise 19
{/* TODO: update this render function to output the post if we have one
or render "Loading" if we don't have a post
*/}
<Post id={this.state.searchId} render={() => null} />
{/* TODO: once you've done that, pull that logic into a PostOutput component
* (hint: you'll find the prop types above) and use that within the render func
*/}
<Post id={this.state.searchId} render={() => null} />
<Post id={this.state.searchId} children={() => null} />
these are equivalent but one uses the prop `render` and the other `children`
<Post id={this.state.searchId}>{() => null}</Post>
but we can also pass the children prop in implicitly.
This is the same as using the `children=` prop syntax!
(note that exercise 20 is broken at the start on purpose! Look at 20.js to see what to do)
Exercise 20
<Post id={this.state.searchId}>
{post => <PostOutput post={post} />}
</Post>
<Post
id={this.state.searchId}
render={post => <PostOutput post={post} />}
/>
can we make Post support both of these?
Any questions? :)
https://github.com/jamiebuilds/create-react-context
this.state.foo = 1
this.state.foo = 1
hey, I need to know `foo` !
this.state.foo = 1
foo={this.state.foo}
foo={this.props.foo}
foo={this.props.foo}
foo={this.props.foo}
this.state.foo = 1
foo={this.state.foo}
foo={this.props.foo}
foo={this.props.foo}
foo={this.props.foo}
this.state.foo = 1
foo={...}
the 3 middle components
don't know or care about foo.
const Context = createReactContext(defaultValue);
// <Context.Provider value={providedValue}>{children}</Context.Provider>
// ...
// <Context.Consumer>{value => children}</Context.Consumer>
<ThemeContext.Provider value={this.state.theme}>
{this.props.children}
</ThemeContext.Provider>
// and then later on in any child component
<ThemeContext.Consumer>
{theme => (
<h1 style={{ color: theme === 'light' ? '#000' : '#fff' }}>
{this.props.children}
</h1>
)}
</ThemeContext.Consumer>
The component that makes a value accessible to all consumers in the component tree.
The component that can read the value of a piece of state.
<MyBlog /> this.state.signedIn
<Posts />
<Post post={post} />
<UserActions />
<MyBlog /> this.state.signedIn
<Posts />
<Post post={post} />
<UserActions />
user actions needs to know if the user is signed in, to know if they can perform the actions
import createReactContext from 'create-react-context'
const AuthContext = createReactContext(false)
export default AuthContext
default value
import AuthContext from './auth-context'
<div>
<h1>Blog posts by Jack</h1>
<AuthContext.Provider value={this.state.signedIn}>
<Posts />
</AuthContext.Provider>
</div>
value for any consumers
any consumer in <Posts /> or below can now consume our auth context
import AuthContext from './auth-context'
<AuthContext.Consumer>
{signedIn => (
<div>signed in? {signedIn === true ? 'yes' : 'no'}</div>
)}
</AuthContext.Consumer>
Exercise 21!
class MyBlog extends Component {
state = {
signedIn: false,
}
signIn = () => {
this.setState({ signedIn: true })
}
signOut = () => {
this.setState({ signedIn: false })
}
render() {
return (
<div>
<header>
{this.state.signedIn ? (
<Fragment>
<span>Signed in as jack</span>
<button onClick={this.signOut}>Sign Out</button>
</Fragment>
) : (
<button onClick={this.signIn}>Sign In</button>
)}
</header>
<div>
<h1>Blog posts by Jack</h1>
<AuthContext.Provider value={this.state.signedIn}>
<Posts />
</AuthContext.Provider>
</div>
</div>
)
}
}
using context in our blog post app
Exercise 22
our app currently only exposes if the user is signed in or not. Can we make it so the user can sign in with a username, and then we show that username throughout the site?
Add a small input to the header so a user can "sign in" with their username, and then add the user's username to the `AuthContext` so it can be used in our application.
https://github.com/facebookincubator/create-react-app
yarn global add create-react-app npm install --global create-react-app
create-react-app my-app cd my-app && yarn start
github-proxy-api.herokuapp.com /users/jackfranklin/repos
github-proxy-api.herokuapp.com /users/jackfranklin/repos
Use the code "RENDERCONF" for 50% off :)
https://javascriptplayground.com/testing-react-enzyme-jest/