📌 Learning objectives:
- learn how to write React components
- understand the difference between state and props
- learn how to handle events
- know how to use existing React components
Create React apps with no build configuration:
- Start building an app directly
- No configuration hassle
- Great default settings
- Eject mode
$ npm install -g create-react-app
- Install
yarn
: https://yarnpkg.com/lang/en/docs/install/ - Create a new React project:
$ cd react
$ create-react-app seqbook
$ cd seqbook
$ git init
$ git add . && git commit -m "Initial commit"
Seqbook is a DNA sequences book. Users will be able to manipulate sequences, fetch sequences from an online database, and visualize different metrics.
.
├── README.md
├── node_modules/
├── package.json
├── public/
├── src/
└── yarn.lock
JSX is an extension of the JavaScript syntax. More precisely, it is an alias for
React.createElement()
.
It is not HTML, yet it is tag-based. React does not require JSX, but it is much more convenient to use it!
Want to write your own JSX renderer? Read WTF is JSX.
const simpleElement = <div />;
const complexElement = (
<div
message="hello"
value={anything}
>
<p>42</p>
</div>
);
import React from 'react';
React.Component
: allows to create components, i.e. JavaScript classes that can be instantiated with JSXReact.PureComponent
: allows to create read-only components, i.e. components only used for display purpose
import ReactDOM from 'react-dom';
ReactDOM.render()
: allows to render React components to the DOM, i.e. it converts components into HTML- Provides many HTML-like components (DOM tags), e.g.,
<div />
,<span />
, etc
// src/Item.js or src/Item/index.js
class Item extends React.Component {
render() {
return (
<li>42</li>
);
}
}
export default Item;
import React from 'react';
import ReactDOM from 'react-dom';
class Item extends React.Component {
render() {
return (
<li>42</li>
);
}
}
ReactDOM.render(<Item />, document.querySelector('#app'));
// src/Item/index.js
const Item = () => <li>42</li>;
export default Item;
- Fast, simple to read
- Should be preferred
- No lifecycle methods
- No internal state
import React from 'react';
import ReactDOM from 'react-dom';
const Item = () => <li>42</li>;
ReactDOM.render(<Item />, document.querySelector('#app'));
import React from 'react';
import ReactDOM from 'react-dom';
const Item = () => <li>42</li>;
const List = () => (
<ul>
<Item />
<Item />
<Item />
</ul>
);
ReactDOM.render(<List />, document.querySelector('#app'));
Warning: Each child in an array or iterator should have a
unique "key" prop. Check the render method of `List`. See
https://fb.me/react-warning-keys for more information.
A "key" is a special string attribute you need to include when creating lists of elements. Keys help React identify which items have changed, are added or are removed.
Documentation: Lists and Keys
import React from 'react';
import ReactDOM from 'react-dom';
const Item = () => <li>42</li>;
const List = () => (
<ul>
<Item key="1" />
<Item key="2" />
<Item key="3" />
</ul>
);
ReactDOM.render(<List />, document.querySelector('#app'));
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class App extends Component {
render() {
return (
<div className="App">
<div className="App-header">
{/* <img src={logo} className="App-logo" alt="logo" /> */}
<h2>Welcome to React</h2>
</div>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('#app'));
-
Remove a few useless files:
$ git rm src/*.css src/*.svg
-
Remove corresponding
import
(seeyarn start
output) -
Add Bootstrap CSS:
$ yarn add bootstrap@3
// src/index.js import 'bootstrap/dist/css/bootstrap.css'; import 'bootstrap/dist/css/bootstrap-theme.css';
- Create a
Header
component with a Bootstrap navbar (brand image) - Import and use it in the existing
App
component
import React from 'react';
const Header = () =>
<nav className="navbar navbar-default navbar-fixed-top">
<div className="container-fluid">
<div className="navbar-header">
<a className="navbar-brand" href="/">
Seqbook
</a>
</div>
</div>
</nav>;
export default Header;
class App extends Component {
render() {
return (
<div className="App">
<Header />
<div className="container-fluid">
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
</div>
);
}
}
A component can be written in
src/ComponentName.js
src/ComponentName/index.js
It will be equally imported with:
import ComponentName from './ComponentName';
Prefer the directory approach (explained later).
Instead of using relative imports, we can use absolute imports, which is more convenient:
// relative
import Header from './Header';
// absolute
import Header from 'Header';
With Create React App, set the NODE_PATH
variable:
$ echo 'NODE_PATH=src/' > .env
Props are read-only arbitrary inputs.
All React components must act like pure functions with respect to their props. "Pure" functions do not attempt to change their inputs and always return the same results for the same inputs.
In a class-based component, props are accessible via the this.props
attribute:
class Item extends React.Component {
// ...
render() {
return (
<li>{this.props.value}</li>
);
}
}
<Item value=42 />
In a functional-based component, props are the first argument of the function:
const Item = (props) => <li>{props.value}</li>;
const List = ({ values }) => (
<ul>{values.map(val => <Item value={val} />)}</ul>
);
<List values={[42, 'foo', 'bar', 123]} />
$ yarn add prop-types
import PropTypes from 'prop-types';
PropTypes
exports a range of validators that can be used to make sure the data
you receive is valid.
Documentation: Typechecking With PropTypes
class Item extends React.Component {
}
Item.propTypes = {
title: PropTypes.string.isRequired,
};
or
class Item extends React.Component {
static propTypes = {
title: PropTypes.string.isRequired,
};
}
const Item = (props) => <li>{props.title}</li>;
Item.propTypes = {
title: PropTypes.string.isRequired,
};
-
Create a
Item
component rendering atitle
prop -
Create a
List
component that renders a list of elements (namedsequences
). Each entry must be an instance ofItem
:const sequences = ['foo', 'bar', 'baz']; // <List sequences={sequences} />
-
Use Bootstrap list-group style
const List = ({ sequences }) => {
if (sequences.length === 0) {
return <p>no sequences</p>;
}
return (
<ul className="list-group">
{sequences.map(s => <Item key={s} title={s} />)}
</ul>
);
};
List.propTypes = {
sequences: PropTypes.arrayOf(PropTypes.string).isRequired,
};
import React from 'react';
import PropTypes from 'prop-types';
const Item = ({ title }) => (
<li className="list-group-item">{title}</li>
);
Item.propTypes = {
title: PropTypes.string.isRequired,
};
export default Item;
- Add the
List
component to theApp
component in acol-md-4
column (add a fluid container and a row too) - Add a
src/styles.css
file for the body padding (due to Bootstrap navbar)
render() {
return (
<div className="App">
<Header />
<div className="container-fluid">
<div className="row">
<div className="col-md-4">
<h3>Sequences</h3>
<List sequences={['ATCG', 'ATCGATTT']} />
</div>
</div>
</div>
</div>
);
}
State allows React components to change their output over time in response to user actions, network responses, etc. State is similar to props, but it is private and fully controlled by the component.
State can only be used in class-based components.
React provides a setState()
method to act on the state, never mutate it by
yourself.
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = { value: 42 };
}
render() {
return (
<p>State value is: {this.state.value}</p>
);
}
}
ReactDOM.render(<App />, document.querySelector('#app'));
- Move the
sequences
list to theApp
component state
-
Require the
seq-utils
module created previously:$ yarn add file:../seq-utils/seq-utils-<version>.tgz
-
Use this module to populate the
sequences
state attribute with random sequences
Props are chunks of app state that are passed into your component from a parent component.
State is something that changes within a component, which could be used as props for its children.
Yet, both are plain JS objects, deterministic and trigger a render update when they change.
Further explanation: Props vs State
Most components have function props to handle events. Events are "camelCased" and you have to pass functions as event handlers.
When passing functions in JSX callbacks, you have to be careful about the
meaning of this
.
Documentation: Handling Events
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = { value: 42 };
// Uncomment the line below to be able to increment the `value`.
//this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick() {
this.setState(prevState => ({ value: prevState.value + 1 }));
}
render() {
return (
<div>
<p>State value is: {this.state.value}</p>
<button onClick={this.handleOnClick}>increment</button>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('#app'));
Using the Babel transform-class-properties
plugin
(enabled in Create React App).
import React from 'react';
import ReactDOM from 'react-dom';
class App extends React.Component {
constructor(props) {
super(props);
this.state = { value: 42 };
}
handleOnClick = () => {
this.setState(prevState => ({ value: prevState.value + 1 }));
}
render() {
return (
<div>
<p>State value is: {this.state.value}</p>
<button onClick={this.handleOnClick}>increment</button>
</div>
);
}
}
ReactDOM.render(<App />, document.querySelector('#app'));
How to get user's input data with React?
Documentation: Forms
In HTML, form elements (input
, textarea
, etc.) maintain their own state. In
React, we usually keep state in the component. A controlled component is a form
element whose value is controlled by React.
import React from 'react';
import ReactDOM from 'react-dom';
class Search extends React.Component {
constructor(props) {
super(props);
this.state = { search: '' };
}
handleOnChange = event => {
// Commenting the line below should help you understand why
// the `input` is controlled.
this.setState({ search: event.target.value });
};
render() {
return (
<div>
<input
value={this.state.search}
onChange={this.handleOnChange}
type="text"
/>
<p>Value is: {this.state.search}</p>
</div>
);
}
}
ReactDOM.render(<Search />, document.querySelector('#app'));
Uncontrolled components means form data is handled by the DOM itself, but it is rarely useful.
Documentation: Uncontrolled components
- Add a button below the
List
to add new (random) sequences to it - Allow to select a list item and display its information on the right side
-
Create a
Sequence
component that renders a DNA sequence (PropTypes.string
):<pre> <code>{dna}</code> </pre>
-
Add a
styles.css
file for this component with the following content:.Sequence { word-break: normal; }
-
Use it on the right side of the interface
- Decompose your UI into different main components (e.g.,
Header
,Footer
) - Break each main component into smaller, specialized, components
- Create small components that are reusable (e.g.,
Button
,Card
)
Let's have fun with React components!
- Create a
Card
component (insrc/ui/
) that can display a title, a value and optionally a unit. Values can be strings or numbers - Use Boostrap
panel
to style thisCard
component
- Create a
SequenceView
class-based component that takes asequence
object as prop. It should render the name of the sequence and the sequence of nucleotides using theSequence
component - Use it in your
App.js
- Create a
Length
component (insrc/widgets/
) that renders aCard
displaying the length of the sequence (PropTypes.string
) - Add it to your
SequenceView
component
Create another widget component GCContent
that renders a Card
. The title
should be "GC content" and the unit "%". Place it next to the Length
widget.
The value, which will be computed in the next exercise, requires the dna
sequence.
Add a method getValue()
to the GCContent
that computes the GC
content of the sequence string given
to that component. The displayed value will be a percentage.
You should call contentATGC()
on a Nt.Seq
instance, then compute the result
of the GC content formula:
const atgc = seq.contentATGC();
const gc = (atgc['G'] + atgc['C']) /
(atgc['A'] + atgc['T'] + atgc['G'] + atgc['C']);
Each component has several lifecycle methods that you can override to run code at particular times in the process:
- Mounting: when an instance of a component is being created and inserted into the DOM
- Updating: when a component is being re-rendered, triggered by changes to props or state
- Unmounting: when a component is being removed from the DOM
UNSAFE_componentWillMount
UNSAFE_componentWillReceiveProps
UNSAFE_componentWillUpdate
- `getDerivedStateFromProps` - `getSnapshotBeforeUpdate`
There should be a single source of truth for any data that changes in a React application.
Most of the time, several components need to reflect the same changing data. Instead of duplicating the data, lift the shared state up to the closest common ancestor.
C
and D
share the same data via "root" as the common ancestor. This
introduces coupling but Redux solves it.
Every React component has a special className
attribute to add CSS classes to
it, you cannot use class
.
const Component = (props) => (
<div className="Component">
// ...
</div>
);
It is advised to use the same className
as the component name but you can do
whatever you like.
The classnames library is very useful to deal with different CSS class names in JavaScript:
import classNames from 'classnames';
classNames('foo', 'bar');
// => 'foo bar'
classNames('foo', { bar: true });
// => 'foo bar'
classNames('foo', { bar: false });
// => 'foo'
Create React App bundles Jest and you can
run the test suite with yarn test
(calling jest
directly won't work).
Also known as "better than nothing" testing. It uses a test renderer to quickly generate a serializable value for your React tree and compares it to a reference.
$ yarn add --dev react-test-renderer
Documentation: Snapshot testing
// src/Item/index.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Item from './Item';
it('renders correctly', () => {
const tree = renderer
.create(<Item title="item title" onSelect={() => {}} />)
.toJSON();
expect(tree).toMatchSnapshot();
});
Jest creates snapshot reference files that must be put under version control!
$ cat src/Item/__snapshots__/index.test.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<button
className="list-group-item"
onClick={[Function]}
>
item title
</button>
`;
If you modify your component, snapshot tests will not pass. You have to update the snapshot reference files:
$ yarn test --updateSnapshot
Snapshot Summary
› 1 snapshot updated in 1 test suite.
Enzyme is a JavaScript Testing utility for React, by Airbnb.
$ yarn add enzyme enzyme-adapter-react-16 --dev
// src/setupTests.js
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
Documentation: http://airbnb.io/enzyme/
shallow()
: use it if you want to test components in isolation from the child components they rendermount()
: useful when you have components that may interact with DOM APIs, or may require the full lifecycle in order to fully test the componentrender()
: I do not think I have ever used it
// src/List/index.test.js
import React from 'react';
import { shallow } from 'enzyme';
import { generate } from 'seq-utils';
import List from './index';
import Item from './Item';
it('renders items', () => {
const sequences = [ generate(), generate() ];
const wrapper = shallow(
<List sequences={sequences} onSelectSequence={() => {}} />
);
expect(wrapper.find(Item)).toHaveLength(sequences.length);
});
// src/List/index.test.js
it('receives event when Item is selected', () => {
const sequence = generate();
const spy = jest.fn();
const wrapper = mount(
<List sequences={[sequence]} onSelectSequence={spy} />
);
expect(spy).not.toHaveBeenCalled();
wrapper.find('button').simulate('click');
expect(spy).toHaveBeenCalled();
});
Use the command below to generate code coverage:
$ yarn test --coverage
You can open coverage/lcov-report/index.html
to get the HTML report (which is
always generated).
- Add snapshot tests for the following components:
Card
,Item
andLength
- Add tests with
shallow()
andmount()
for theList
component - Try to improve the overall code coverage (if time allows)
Create a FractionalContent
widget that renders a pie chart showing the content
of a sequence, using Victory.
Data for the chart can be retrieved by calling fractionalContentATGC()
on a
Seq
instance. This method returns a JavaScript hash map:
{
'A': 0.375,
'T': 0.375,
'G': 0.125,
'C': 0.125
}
Create a Complement
widget that renders a Sequence
configured with the
complement of a sequence passed to Complement
as a prop.
You can get the complement of a Seq
instance by calling complement()
on it,
and get the DNA sequence as a string by calling sequence()
.