Things you should know about React

What are the everyday problems of a JS dev today?

Problem one:

too many choices

Notes: lots of solutions for the same problem and easily forgetting about the previous ones; staying alone amongst dozens of blogs, websites can be a burden

Problem two:

very short lifespan

Grunt? Already dead. Gulp? Dead. Bower? Dead. Just to name a few.

Notes: replaced by npm + webpack, webpack also replaced browserify

Problem three:

UI complexity

Notes: user expectations: fast, performant apps giving him pleasure of using it, not only solving his problems; it leads to fat clients and simply complex UI

Problem four:

tools complexity (also related to JavaScript fatigue)

Notes: React as a potencial solution for solving UI problems at scale

Notes: important to choose libraries that can be easy to replace when the time comes (e.g. react)

What do we want as frontend devs?

  • lightweight DOM

  • control over our data

  • simple code with no magic behind

Notes: lightweight DOM = to create smooth animations, performant updates and thus good UX, 2nd = don't waste time on synching stuff, debugging implicit code;

3rd = simply no framework based on cheats and tricks to solve problems

Let me introduce React:

an unopinionated library (not a framework) that will solve your UI problems at scale

What makes React shiny?

Performance

Abstraction

Notes: performance is rarely a benefit, virtual DOM solves the problem with DOM being modified when it's not necessary; the real benefit lies behind abstraction

Components

Everything can be a component!

Notes: UI is basically a set of small (at best loosely coupled) components

Notes: divding a view into components

Common problems

Notes: when having a mockup: draw lines or color parts of it in order to set clear boundaries between components before implementing a view; subjective and little bit stiff measurements: around 100 lines per component, 10-20 regarding JSX at best

Smart vs. dumb components

or stateful containers vs. stateless function components

Notes: stateful: controling state modifications, having access to all lifecycle methods inherited from Component class; stateless: usually functions with no explicit access to state inside

import React, { Component } from 'react';

class NewsContainer extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isClicked: false
    };
  }
  
  handleClick = () => {
    this.setState({
      isClicked: true
    });
  }
  
  render() {
    return (
      <NewsFeed onClick={this.handleClick} />
    );
  }
}

Smart component

const NewsFeed = ({ onClick }) =>
  <div>
    <span>NewsFeed</span>
    <button onClick={onClick}>Click</button>  
  </div>

Dumb component

Why does it matter?

Notes: clean separation between logic view (smart components) and rendering view (dumb components); containers more difficult to test (bloated test suites); dumb components more performant;

Common problem: when should something be smart and when dumb

Notes: usually 1 container for a tree branch (5-10% tops); but: avoid creating huge containers and tons of dumb components - avoid extremes

Higher-order component (HoC)

class CommentList extends Component {
    render() {
	return (
	    <div>
                {this.props.comments.map(comment =>
		    <div>{comment}</div>
		)}
	    </div>
	);
    }
}

Notes: we have a list of comments in our app but we want also another one with data being rendered within

class CommentListWithSubscription extends Component {
  constructor() {
    super();
    this.state = {
      comments: DataSource.getComments()
    };
  }

  componentDidMount() {
    DataSource.addChangeListener(this.handleChange);
  }

  componentWillUnmount() {
    DataSource.removeChangeListener(this.handleChange);
  }

  handleChange() {
    this.setState({
      comments: DataSource.getComments()
    });
  }

  render() {
    return <CommentList comments={this.state.comments} />;
  }
}
function withSubscription(WrappedComponent) {
  return class CommentListWithSubscription extends Component {
    constructor(props) {
      super(props);
      this.state = {
        comments: DataSource.getComments()
      };
    }

    componentDidMount() {
      DataSource.addChangeListener(this.handleChange);
    }

    componentWillUnmount() {
      DataSource.removeChangeListener(this.handleChange);
    }

    handleChange = () => {
      this.setState({
        comments: DataSource.getComments()
      });
    }

    render() {
      return (
        <WrappedComponent comments={this.state.comments} />
      );
    }
  };
}

Notes: the previous one can be transformed to HoC and used as either wrapping function or class decorator (depending on needs)

@withSubscription
class CommentList extends Component {
    render() {
	return (
	    <div>
                {this.props.comments.map(comment =>
		    <div>{comment}</div>
		)}
	    </div>
	);
    }
}
@withSpinner
class CommentList extends Component {
    render() {
	return (
	    <div>
                {this.props.comments.map(comment =>
		    <div>{comment}</div>
		)}
	    </div>
	);
    }
}

Notes: common usage of HoC: spinner; use when you want to decide whether component should be rendered or the render should be delayed until the data arrives from the server

This is a very popular alternative to deprecated mixins and used in Redux and Relay

JSX

Notes: one of the reason why JSX was invented: limiting context switch between templates and view logic leading to more productive developers; note JSX is optional in react

Common problems with JSX

format of JSX

Notes: JSX should be easy to read; we keep forgetting about: wrapping text nodes with html tags, formatting props and we keep leaving garbage: computations inside JSX and too many conditions

too many conditions

Notes: many different variations of components introduced with conditions inside JSX leads to unnecessary complexity; better refactor for smaller components, use HoC, optional props for extending behaviour, etc.

logic inside JSX - should stay as pure as it can be

Notes: logic makes it less readable and easily introduce bottlenecks in your components

wrong usage - treating JSX as a template engine, whereas it's not

Notes: if JSX is bloated then the component isn't a good one and should be refactored (e.g. by dividing into smaller ones); components are not used to render big templates

Virtual DOM

Notes: used to tracking changes in the DOM and rerender nodes the most performant way

Common problems

wasting time on rerendering lists

Notes: rendering huge lists will lead to bottlenecks sooner or later regardless of the technology used

shouldComponentUpdate

Notes: used to compute whether component should be rerendered

// news passed from props: [{id: 1, name: 'first'}, {id: 2, name: 'second'}]

class NewsFeed extends Component {
  shouldComponentUpdate() {
    return true; // basic behaviour
  }

  render() {
    const { news, onClick } = this.props;
    
    return (
      <div>
        <span>NewsFeed</span>
        <button onClick={onClick}>Click</button>
        {news.map(newsItem =>
          <div>{newsItem.name}</div>
        )}
      </div>
    );
  }
}

What is going to happen if I click the button (besides invoking onClick prop)?

Notes:  on every click the component's gonna be rerendered so the map will be invoked on each click, which doesn't make sense, because the click handler may have not changed the news array

class NewsFeed extends Component {
  shouldComponentUpdate(nextProps) {
    if (this.props.news.length !== nextProps.news.length) {
      return true;
    }

    return this.props.news.find(news => {
      return nextProps.news.find(nextNews => {
        return news.id === nextNews.id && news.name !== nextNews.name;
      });
    });
  }

  render() {
    const { news, onClick } = this.props;

    return (
      <div>
        <span>NewsFeed</span>
        <button onClick={onClick}>Click</button>
        {news.map(newsItem =>
          <div>{newsItem.name}</div>
        )}
      </div>
    );
  }
}

Notes: rougly correct implementation of previous shouldcomponent update

problems:

complex structures

& mutations

Notes: shouldcomponentupdate is tough to implement and maintain for complex structures; when collections are mutated instead of being immutable shouldcomponent update is pointless and won't work properly (weird bugs tricky to spot)

solution:

Immutable.js

import React, { Component } from 'react';
import { fromJS } from 'immutable';

class NewsContainer extends Component {
  // ...
  
  render() {
    return (
      <NewsFeed onClick={this.handleClick} news={fromJS(this.state.news)} />
    );
  }
}

class NewsFeed extends Component {
  shouldComponentUpdate(nextProps) {
    return nextProps.news.equals(this.props.news);
  }

  render() {
    const { news, onClick } = this.props;

    return (
      <div>
        <br/>
        <span>NewsFeed</span>
        <button onClick={onClick}>Click</button>
        {news.map(newsItem =>
          <div>{newsItem.name}</div>
        )}
      </div>
    );
  }
}

Notes: only equals needed; fromjs transforms arrays to lists and objects to maps by default and tracks changes with hashes

rendering lists in dedicated components

Notes: do not map over in the same component you render other data, but put it inside separate component (especially very important for mobx to be efficient); updating data inside component with map will cause the map to be invoked even when it shouldnt be

doing DOM manipulations outside React scope

Notes: virtual DOM is pointless then

How to deal with other perf problems?

React Perf tools

Notes: especially print wasted prints milliseconds wasted on rendering; there are some plugins on github/npm to ease the pain of using perf tools directly

Don't user array indexes as keys

// bad

{todos.map((todo, index) =>
  <Todo {...todo}
    key={index} />
)}

// better

{todos.map((todo) =>
  <Todo {...todo}
    key={todo.id} />
)}

// id can be generated for instance with node.uuid

Notes: react identifies elements by keys; inserting or removing element in the middle of a list won't make react to think something has changed because the key stays the same

Bind functions early (99% time NOT in JSX)

Notes: binding inside JSX or render would make you waste one function call on each rerender; especially crucial for animating components

class NewsFeedContainer extends Component {
    constructor() {
        this.handleClick = this.handleClick.bind(this); // good
    }

    @autobind // good
    handleClick = () => { // a little bit nasty, but readable
        // ...
    }

    render() {
        return (
            <NewsFeed onClick={this.handleClick.bind(this)} /> // bad
        );
    };
}

1%??

Notes: what about 1% one exception, but also could have been refactored to separate function calling function from props

class NewsFeed extends Component {
  // ...

  render() {
    const { news, onClick } = this.props;

    return (
      <div>
        <br/>
        <span>NewsFeed</span>
        <button>Click</button>
        {news.map(newsItem =>
          <NewsItem
            onClick={onClick.bind(null, newsItem.id)}
          >
            {newsItem.name}
          </NewsItem>
        )}
      </div>
    );
  }
}

sort of... but not the best way

observables

Notes: observable: collection that arrives after time; your component needs to be an observer; observables kept your data in sync across multiple components out of the box

Next slide: mobx example; computed properties can be referenced as values inside components not functions; you can stop tracking changes for observables if you want in your component; use actions instead of modifying values directly

class DummyStore {
	@observable firstName = {
		value: '',
		@computed get isValid() {
			return !this.errors.length;
		},
		errors: []
	};
	@observable lastName = {
		value: '',
		@computed get isValid() {
			return !this.errors.length;
		},
		errors: []
	};

	@action changeFirstName(firstName) {
		// ...
	}

	@action changeLastName(lastName) {
		// ...
	}

	@computed get isValid() {
		return this.firstName.isValid && this.lastName.isValid;
	}
}

debouncing functions

Notes: debounce function to simply time out execution of them when getting data from the server on each specific event

debounce is rarely needed in view layer

Notes: debounce action that calls the server endpoint instead of onChange handler inside your component

Avoid refs at any cost (http://stackoverflow.com/a/29504636)

Other general bad practices

Introducing behaviour in component's name

Notes: leave it to props, HoC or decorators; do not use something like Select and then SelectWithLabel

Modifying state directly instead of invoking props handlers

Notes: more difficult to reason about data flow and too many stateful components

Using context too excesively

Notes: you can use context to pass props implicitly, but it leads to more difficult reasoning about data flow; be better explicit even if it looks a little bit like boilerplate code

Missing propTypes validation

Notes: easier to refactor when having props properly validated; you know exactly about component's expectations by just looking at propTypes instead of manually debugging a few components to get it; you will get errors if props of invalid type are passed

Getting your data too late

Notes: usually in componentwillmount but some people are getting data in render function or componentdidupdate or didmount which is weird and problematic. one benefit of c omponentwillmoount is that you can change the state before the initial render.

A few words about testing

Enzyme

Notes: a great library for rendering components in your tests and traversing the markup to make proper assertions; also emulating events when shallow rendering (which is impossible with plain react test utilities); shallow render should be used at best, because it's performant and renders exactly the thing you wish to unit test

describe('Button component', () => {
	const props = {
		onClick: jasmine.createSpy(),
	};

	it('should render Button', () => {
		const button = shallow(<Button {...props} />);
		expect(button.hasClass(style.root)).toBeTruthy();
		expect(button.type()).toBe('button');
	});

	it('should render children when passed in', () => {
		const button = shallow(
			<Button {...props}>
				Button Text
			</Button>
		);
		expect(button.contains('Button Text')).toBeTruthy();
	});

	it('should handle click event', () => {
		const button = shallow(<Button {...props} />);

		button.simulate('click');
		expect(props.onClick).toHaveBeenCalled();
	});
});

TDD

Notes: can help you be more productive and to better understand the feature when writing unit tests for components in TDD; takes time to get used to it, but very rewarding in the end

Thank you!

things you should know about react

By Przemek Murawski

things you should know about react

  • 324

More from Przemek Murawski