react-immersion-ws



react-immersion-ws

0 1


react-immersion-ws


On Github RobinThrift / react-immersion-ws

React Immersion Workshop

Robin Thrift
Jr. Dev, NewStore
@RobinThrift

Outline

  • Basics
  • Components
  • State
  • Props
  • Refs
  • Context
  • Lifecycle Methods
  • Testing
  • Flux
  • React Native APIs
  • TypeScript

Basics

“A JavaScript library for building user interfaces”

  • Model
  • View
  • Controller
  • Model
  • View
  • Controller

Everything Is A Component

“Reduce Coupling, increase Cohesion”

Immutability

  • no bindings
  • no DOM events
  • when the Compontents change:rerender entire DOM

Virtual DOM

It’s just like the real DOM

But fast!

  • mimics the real DOM
  • renders only the difference

diff: (Tree, Tree) → ∆Tree

Tree Diffing is usually ∈ O(n3)!

By using heurstics, Reacts Diffing Algorithm ∈ O(n)More

JSX

render() {
    return <div>Hello World, at {new Date().toString()}</div>;
}

⟱ JSX Transformer ⟱

render() {
    return React.createElement('div', {}, 
        'Hello World, at ', new Date().toString());
}
class App extends React.Component {
    render() {
        return (
            <div className="main">
                <div className="picture-bg">
                    <img src={getRandomBg()} />
                </div>
                <Clock />
                <CmdLine ps1="λ" />
                <LinksBox />
            </div>
        );
    }
}
class Clock extends React.Component {
    render() {
        let time = moment().format(this.props.format);
        return (<time>{time}</time>);
    }
}
// ...
    render() {
        return (<div><Clock format="HH:mm" /></div>);
    }
// ...

Let’s Talk About Components

Everything Is A Component

Components can be composed

Components are meant to be composed

Throw away extends*

*almost

STATELESS COMPONENTS

Stateless Components

export let LookMaNoState = (props) => {
    return (<span>{props.name}</span>);
}
React.renderDom(<LookMaNoState name="Mark">, mountNode);

This is the recommended pattern, when possible.

Mutating State is the root of all evil

What if I do need state?

Do I really?

Yes!

Fine.

Let’s deal with state.

What does belong in my component’s state?

  • internal data
  • that is changed by internal actions

What does not belong in my component’s state?

  • external data
  • like Props (especially complex types)

When do I setState()?

  • event handlers
  • componentWillReceiveProps

Props

  • define a components API
  • input and output
  • not bindings

Things to consider

  • the component should be generic
  • and flexible

Bad Example

class ProductInfiniteScroller extends React.Component {
    scrollHander() {...}
    dataLoader() {...}
    reposition() {...}
    removeClippedSubviews() {...}
    render() {
        return (
            <ul>
                {this.state.item.map((i) => {
                    return (<span>{i.name}</span>);
                })}
            </ul>
        );
    }
}

Good Example

React Native’s ScrollView
<ScrollView
    horizontal={true|false}
    bounce={true|false}
    paging={true|false}>
</ScrollView>

Input

  • configure the component
  • give it the relevant data
  • prefer simple types
<ProductPrice data={data} />
<ProductPrice 
    listPrice={data.price.list}
    markdownPrice={data.price.markdown} />

Output

aka callbacks

Bad Example

class MyComponent extends React.Component {
    onClickHandler() {
        this.props.user.likes += 1;
    }
    render() {
        // ...
    }
}

Good Example

class MyComponent extends React.Component {
    onClickHandler() {
        this.props.onLike(user.props.likes + 1);
    }
    render() {
        // ...
    }
}

Immutability

  • props are immutable
  • in theory, only
  • which makes it a bit unsafe

Immtuable.js Record

import {Record} from 'immutablejs';
let FooBar = Record({foo: 'baz', bar: 'blub'});
// ...

let fb = new FooBar({foo: 'lorem'});
fb.get('foo') // -> 'lorem'

let changed = fb.set('foo', 'bar');
fb.get('foo') === changed.get('foo') // -> false

A few more things to consider

Setting state from props in constructor is an anti-pattern
class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            data: props.data
        }
    }
}

Use the prop directly if you need to!

Use componentWillReceiveProps to update state when props changed
class MyComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        this.setState({
            data: nextProps.data
        });
    }
}

You cannot call setState in componentWillUpdate!

Use prop types (and default props)

  • there are many
  • use them
  • not only for primitives
MyComponent.propTypes = {
    someString: React.PropTypes.string,
    someEnum: React.PropTypes.oneOf(['A', 'B']),
    someArrayOf: React.PropTypes.arrayOf(React.PropTypes.number)
}
Children are also props (kind of)
  • prefer child components over arrays
  • use cloneElement when modifying a child component
    <Slider images={[....]} />
    
<Slider>
    <Image />
    <Image />
    <Image />
</Slider>
Transfer generic props down the tree

Bad:

class RxButton extends React.Component {
    render() {
        return (
            <TouchableHightlight>
                {this.props.children}
            </TouchableHightlight>
        );
    }
}

<RxButton accessibilityLabel="button">
    // ...
</RxButton>
Transfer generic props down the tree

Good:

class RxButton extends React.Component {
    render() {
        return (
            <TouchableHightlight
                accessibilityLabel={this.props.accessibilityLabel}>
                {this.props.children}
            </TouchableHightlight>
        );
    }
}
<RxButton accessibilityLabel="button">
    // ...
</RxButton>
Pass along all the props when wrapping a component
class RxButton extends React.Component {
    render() {
        return (
            <RxButton {...this.props}>
                <RxIcon src="..." />
            </RxButton>
        );
    }
}
<RxIconButton accessibilityLabel="button">
    // ...
</RxIconButton>

Let’s Talk About Context

Don’t use context*

“Using context makes your components more coupled and less reusable”

What is context?

  • describes a components surroundings
  • quasi globals (but safer*)

*Use cases for context

  • theming
  • router(?)

Using context

class MyWrapper extends React.Component {
    getChildContext() {
        return {
            mainColor: '#bada55'
        };
    }
}

Using context

class MyButton extends React.Component {
    render() {
        return (
            <button style={{backgroundColor: this.context.mainColor}}>
                {this.props.children}
            </button>
        );
    }
}

This won’t work!

console.log(this.context);
// => {}

Context is safe(ish)

class MyWrapper extends React.Component {
    getChildContext() {
        return {
            mainColor: '#bada55'
        };
    }
}

MyWrapper.childContextTypes = {
    mainColor: React.PropTypes.string
};

Context is safe(ish)

class MyButton extends React.Component {
    render() {
        return (
            <button style={{backgroundColor: this.context.color}}>
                {this.props.children}
            </button>
        );
    }
}

MyButton.contextTypes = {
    mainColor: React.PropTypes.string
};

*Use cases for context

  • theming
  • navigator/router (maybe)

Quiz Time!

class MyWrapper extends React.Component {
    getChildContext() {
        return this.state;
    }
    constructor(props) {
        super(props);
        UserSerivce.getUserByRoute(this.context.currentRoute)
            .then((user) => {
                this.setState({user});
            });
    }
    render() {
        return (<Profile />);
    }
}
assume contextTypes are set correctly
class Profile extends React.Component {
    // ... addAsFriend()
    render() {
        return (
            <div className="user-profile">
                <h3>{this.context.user.name}</h3>
                <span>
                    {`Friends: ${this.context.user.friendCount}`}
                </span>
                <button onClick={this.addAsFriend.bind(this)}>
                    Add As Friend
                </button>
            </div>
        );
    }
}
class Profile extends React.Component {
    addAsFriend() {
        this.context.user
                .addUser(this.context.currentUser);
    }
    // ... render()
}

Problems?

How could this be improved?

class MyWrapper extends React.Component {
    constructor(props) {
        super(props);
    }
    // data retrieval with Flux
    render() {
        return (<Profile user={this.state.user} />);
    }
}
class Profile extends React.Component {
    // ... addAsFriend()
    render() {
        return (
            <div className="user-profile">
                <h3>{this.props.user.name}</h3>
                <span>
                    {`Friends: ${this.props.user.friendCount}`}
                </span>
                <button onClick={this.addAsFriend.bind(this)}>
                    Add As Friend
                </button>
            </div>
        );
    }
}

addAsFriend() with Flux

Refs

Old Way

<input ref="username" type="text" value={this.props.username} />
// ...
console.log(this.refs.username)
// => HTMLElement
<MaterialTextInput ref="textinput" value={this.props.value} />
// ...
console.log(this.refs.textinput)
// => MaterialTextInput
console.log(ReactDOM.findDOMNode(this.refs.textinput))
// => HTMLElement

Since 0.14

class RefTest extends React.Component {
    render() {
        return (
            <input
                ref={(i) => { this.input = i; }}
                type="text"
                value={this.props.username} />
        );
    }
}

Callback is executed immediately after the component is mounted

Still resolves to HTMLElement or React Class

Callback will be called with null as argument, when component is unmounted

React Lifecycle

The componentDidMount() method of child components is invoked before that of parent components

all above are not called on initial render

Testing

Testing React (Web)

  • What am I testing?
  • How deep do I need to go?
  • Do I need the DOM?

React Test Utils

import ReactTestUtils from 'react-addons-test-utils'

Full Docs

Assertion Helpers

  • isElement(element)
  • isElementOfType(element, ComponentClass)
  • findAllInRenderedTree(tree, testFn)
  • scryRenderedComponentsWithType(tree, ComponentClass)
  • findRenderedComponentWithType(tree, ComponentClass)
  • … and others
Last 3 are still a bit buggy
import {isElementOfType, isElement} from 'react-addons-test-utils';

test('existence', () => {
    expect(isElement(<MyComponent />)).to.be.true;
    expect(isElementOfType(<MyComponent />, MyComponent)).to.be.true;
});
Source

Shallow Rendering

let shallowRenderer;
setup(() => {
    shallowRenderer = createRenderer();
});

test('ensure correct sub components', () => {
    shallowRenderer.render(<MyComponent />)
    let output = shallowRenderer.getRenderOutput();
    expect(
        isElementOfType(output.props.children, MySubComponent)
    ).to.be.true;
});
Source

DOM Testing

Problems with traditional DOM testing

  • cumbersome
  • difficult to set up and tear down
  • requires a browser
  • it’s slow

DOM Testing With JSDOM

var jsdom = require('jsdom');
var document = jsdom.jsdom('<!doctype html><html><body></body></html>');
var window = document.defaultView;

global.document = document;
global.window = window;

for (var key in window) {
    if (!window.hasOwnProperty(key)) {
        continue;
    } else if (key in global) {
        continue;
    } else {
        global[key] = window[key]
    }
}

renderIntoDocument(componentInstance)

import {Simulate} from 'react-addons-test-utils';
let node = renderIntoDocument(<MyComponent />);
function renderIntoDocument(instance) {
    var div = document.createElement('div');
    return ReactDOM.render(instance, div);
}

Nothing is actually rendered into the document.

This is great for test isolation!

findRenderedDOMComponentWithTag(node, tagName)

import {findRenderedDOMComponentWithTag} from 'react-addons-test-utils';
findRenderedDOMComponentWithTag(node, 'button');
// returns DOM node or throws error

findRenderedDOMComponentWithTag(node, tagName)

import {findRenderedComponentWithType} from 'react-addons-test-utils';
findRenderedComponentWithType(node, MySubComponent);
// returns instance node or throws error
// only works with fully 'rendered' nodes (not shallowly rendered)

Simulate

import {Simulate} from 'react-addons-test-utils';
Simulate.click(clickableNode);
// clickableNode is 'real' DOM node that has a onClick event, 
// i. e. <button>
Simulate.change(inputNode);
Simulate.keyDown(inputNode, {key: 'Enter', keyCode: 13});
// Simulate.{eventName}(node, eventData);
// for every event that React supports
clickHandler() {
    this.setState({hide: true});
}

render() {
    if (this.state.hide) {
        return (<div></div>);
    } else {
        return (
            <div>
                <MySubComponent 
                    name="testing" 
                    onClick={this.clickHandler.bind(this)} />
            </div>
        );
    }
}
test('simulate click', () => {
    let node = renderIntoDocument(<MyComponent />);
    Simulate.click(findRenderedDOMComponentWithTag(node, 'button'));
    expect(findRenderedDOMComponentWithTag(node, 'div')).to.be.defined;
    expect(
        findRenderedDOMComponentWithTag
            .bind(undefined, node, 'button')
    ).to.throw(Error);
});
Source
test('simulate click', () => {
    let node = renderIntoDocument(<MyComponent />);
    Simulate.click(findRenderedDOMComponentWithTag(node, 'button'));
    expect(findRenderedDOMComponentWithTag(node, 'div')).to.be.defined;
    expect(
        findRenderedDOMComponentWithTag
            .bind(undefined, node, 'button')
    ).to.throw(Error);
});
Source

Testing React (Native)

Things to note

  • do your unit tests in pure JS
    • keep the interaction between iOS and JS small as possible
  • keep integration tests to a minimum
  • UI tests are finicky

How do I make my components testable?

  • use accessibilityLabel-prop liberally
  • use accessible={true}-prop where appropriate

What does accessible={true} mean?

UI Testing (using XCode)

DEMO

<View>
    <Slider>
        <Image resizeMode='contain' source={{uri: images[0]}} />
        <Image resizeMode='contain' source={{uri: images[1]}} />
        <Image resizeMode='contain' source={{uri: images[2]}} />
    </Slider>
</View>
<ScrollView
    accessibilityLabel="slider"
    horizontal={true}
    ...>
    {elements}
</ScrollView>
let slider = XCUIApplication().otherElements["slider"]
slider.swipeLeft()
slider.swipeRight()
slider.swipeLeft()
slider.swipeLeft()
XCTAssert(slider.images["image 3"].frame == slider.frame)

Conclusions

  • Swift only
  • A lot of trial and error
  • Hand down accessibility props!
  • Appium will hopefully allow this to be done in JS

Integration Tests (JS with iOS APIs)

Requires 2 parts:

JS Test Case as React Component Swift/Obj-C test file

All Exceptions should be thrown in JS

import {expect} from 'chai';
import React from 'react-native';
let {AppRegistry, View, Component} = React;
import {TestModule} from 'NativeModules';

class TestComp extends Component {
    render() {
        TestModule.markTestCompleted();
        return (<View></View>);
    }
}

AppRegistry
    .registerComponent('ExampleTests', () => { return TestComp });
@implementation ExampleTests
{
  RCTTestRunner *_runner;
}

- (void)setUp
{
  [super setUp];
  _runner = RCTInitRunnerForApp(@"intTestDist/tests.int", nil);
}

- void()testExampleTests
{
    [_runner runTest:_cmd module:@"ExampleTests"]
}
@end
React Immersion Workshop Robin Thrift Jr. Dev, NewStore @RobinThrift