REFINING REACT
TYPES (FLOW)
ANNOTATION
// @flow
function concat(a: string, b: string) {
return a + b;
}
concat("A", "B"); // Works!
concat(1, 2); // Error!
function acceptsBoolean(value: boolean) {
// ...
}
acceptsBoolean(true); // Works!
acceptsBoolean(false); // Works!
acceptsBoolean("foo"); // Error!
ANY vs MIXED
// @flow
function add(one: any, two: any): number {
return one + two;
}
add(1, 2); // Works.
add("1", "2"); // Works.
add({}, []); // Works.
function getNestedProperty(obj: any) {
// Runtime errors BUT not caught by Flow
return obj.foo.bar.baz;
}
getNestedProperty({});
// @flow
function stringify(value: mixed) {
return "" + value; // Error!
}
stringify("foo");
// Figure out the type before any usage
function stringify(value: mixed) {
if (typeof value === 'string') {
return "" + value; // Works!
} else {
return "";
}
}
stringify("foo");
GENERICS
// @flow
function identity(value: string): string {
return value;
}
function identity<T>(value: T): T {
return value;
}
function identity(func: <T>(param: T) => T) {
// ...
}
function identity<T>(value: T): T {
value = "foo"; // Error!
return value; // Error!
}
type Item<T> = {
foo: T,
bar: T,
};
FLOW TYPED
// @flow
declare module 'aws-sdk/dynamodb/document_client' {
import type { Request } from 'aws-sdk/request';
declare type AttributeMap = { [key: string]: AttributeValue };
declare type AttributeName = string;
declare type AttributeValue = mixed;
declare type ConditionExpression = string;
declare type ConsistentRead = boolean;
declare type ExpressionAttributeNameMap = { [key: string]: AttributeName };
declare type ExpressionAttributeValueMap = { [key: string]: AttributeValue };
declare type IndexName = string;
declare type ItemList = AttributeMap[];
declare type Key = { [key: string]: AttributeValue };
declare type KeyExpression = string;
declare type ReturnValue = 'NONE'|'ALL_OLD'|'UPDATED_OLD'|'ALL_NEW'|'UPDATED_NEW'|string;
declare type TableName = string;
declare type UpdateExpression = string;
declare interface DocumentClient {
constructor(): void;
query(params: QueryInput, cb?: (err: Error, data: QueryOutput) => void): Request<QueryOutput>;
update(params: UpdateItemInput, cb?: (err: Error, data: UpdateItemOutput) => void): Request<UpdateItemOutput>;
}
declare interface QueryInput {
TableName: TableName;
IndexName?: IndexName;
Limit?: number;
ConsistentRead?: ConsistentRead;
KeyConditionExpression?: KeyExpression;
ExpressionAttributeNames?: ExpressionAttributeNameMap;
ExpressionAttributeValues?: ExpressionAttributeValueMap;
}
declare interface QueryOutput {
Items?: ItemList;
Count?: number;
ScannedCount?: number;
LastEvaluatedKey?: Key;
}
declare interface UpdateItemInput {
TableName: TableName;
Key: Key;
ReturnValues?: ReturnValue;
UpdateExpression?: UpdateExpression;
ConditionExpression?: ConditionExpression;
ExpressionAttributeNames?: ExpressionAttributeNameMap;
ExpressionAttributeValues?: ExpressionAttributeValueMap;
}
declare interface UpdateItemOutput {
Attributes?: AttributeMap;
}
}
(style) PRACTICES
// @flow
// Names
type foo = string;
type myFoo = string;
type JSONApiEntity = {};
// Functions
type HttpFn = () => Promise<Object>;
type DoProfilePressFn = (profile: Object) => void;
// Interfaces
interface Serializable {
serialize(): string;
}
class Foo implements Serializable {
serialize() {
return '[Foo]';
}
}
ALTERNATIVES
1. Flow is a static type checker for JavaScript
2. TypeScript is a programming language, typed superset of JavaScript that compiled to plain JavaScript
3. Reason is new syntax and toolchain, not new language
4. Kotlin is a statically typed language, provides the ability to target JavaScript by transpiling Kotlin to ECMAScript
5. PropTypes is a runtime type checker for React props
PERFORMANCE
PROD BUILD
// webpack
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')
}),
new webpack.optimize.UglifyJsPlugin()
- Use the production build set NODE_ENV=production
PROFILING
- Temporarily disable all Chrome extensions, especially React DevTools. They can significantly skew results!
- Make sure you’re running the application in the development mode
- Open the Chrome DevTools Performance tab and press Record
- Perform the actions you want to profile. Don’t record more than 20 seconds or Chrome might hang
- Stop recording
- React events will be grouped under User Timing label
(good) PRACTICES
1. Virtualize long lists
2. Use router-based code splitting
3. Avoid functions that cause jank (60 fps)
4. Defer rendering many components (slow devices)
5. Avoid reconciliation (shouldUpdate, didMount, state)
componentDidMount() {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => this.setState({ shouldRender: true }));
});
}
RECONCILATI O(n)
- Two elements of different types will produce different trees
- Developer can hint at which child elements may be stable across different renders with a 'key' prop
...
shouldComponentUpdate(nextProps, nextState) {
return true;
}
...
IMMUTABILITY
...
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
...
const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
const z = x.set('foo', 'bar');
x === y; // false
x === z; // true
(bad) PRACTICES
1. Optimizing React right from the start
2. Server-side rendering for SEO
3. Inline styles & CSS imports
4. Nested ternary operator
5. Closures in React
return () {
<Button onClick={() => console.log('Hello, World!')}>
Click Me
</Button>
}
REDUX (RESELECT)
- Selectors can compute derived data, allowing Redux to store the minimal possible state
- Selectors are efficient. A selector is not recomputed unless one of its arguments changes
- Selectors are composable. They can be used as input to other selectors
MOBX (MUTABILITY)
1. Use many small components
2. Render lists in dedicated components
3. Don't use array indexes as keys
4. Get from pointers values late
5. Bind functions early (closures)
// Fast:
<DisplayName person={person} />
// Slower:
<DisplayName name={person.name} />
TESTING (JEST)
UNIT-TESTS
1. Breeze to setup (zero config)
2. Lightning fast (parallelize, prioritize, cache)
3. One-stop shop (combined experience)
4. Awesome mocks (not canonical)
5. Supports TypeScript (not Flow)
6. Get you covered (built-in reporting)
7. Does snapshots (track UI changes)
8. Delta testing with watcher (refactoring and TDD)
ENZYME
1. Jest as the test runner, assertion and mocking library
2. Enzyme as extra testing utils to interact with elements
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
(faq) SNAPSHOTS
1. Are snapshots written automatically on CI systems?
2. Should snapshot files be committed?
3. Does snapshot testing only work with React?
4. Difference between snapshots and visual regression?
5. Does snapshot testing replace unit testing?
6. Performance of snapshots in speed and size of files?
7. How do I resolve conflicts within snapshot files?
8. Is it possible to apply TDD with snapshot testing?
9. Does code coverage work with snapshot testing?
(why) SNAPSHOTS
1. No flakiness
2. Fast iteration speed
3. Debugging
...
it('should match to the snapshot', () => {
sut = shallow(<Component {...props} />);
expect(sut).toMatchSnapshot();
});
...
(mock) PRACTICES
import React from 'react';
import { shallow } from 'enzyme';
import Component from './component';
jest.mock('./path/module/to/mock');
const mock = require('./path/module/to/mock');
describe('BasicInfo component: ', () => {
let sut;
beforeEach(() => {
jest.resetAllMocks();
mock.method.mockReturnValue(jest.fn());
});
it('should show toaster notification for success', () => {
sut = shallow(<Component {...props} />);
sut.setState({ key: 'value' });
sut.instance().onHandler();
expect(notification.notify).toHaveBeenCalled();
});
});
STORYBOOK
CONCEPT
1. Development environment for UI components
2. The ability to test rendering for different cases
3. A built-in style guide that updates with your code
4. Easy cross-browser visual checks
5. Interaction manual testing
6. Storyshots for snapshot testing
7. Imageshots for for visual testing (alpha)
(code) PRACTICE
import React from 'react';
import { storiesOf } from '@storybook/react';
import { text } from '@storybook/addon-knobs';
import { withInfo } from '@storybook/addon-info';
import Component from './Component';
import Item from './Item';
storiesOf('Component', module)
.add('Story #01',
withInfo('story #01 info')(() => (
<Component data={[...]}>
<Item title={text('Title1')}>
</Component>
))
);
ADDONS
1. Decorators (wrappers, storybook decorators)
2. Native addons (actions, links and more)
import { configure, addDecorator } from '@storybook/react';
import { withOptions } from '@storybook/addon-options';
addDecorator(
withOptions({
name: 'Refining React',
goFullScreen: true,
showAddonsPanel: true,
showSearchBox: false
})
);
(redux) PRACTICE
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Provider } from 'react-redux';
import { browserHistory } from 'react-router';
import configureStore from './store/configureStore';
const store = configureStore(browserHistory);
storiesOf('Component', module)
.addDecorator(story => (
<Provider store={store}>
{ story() }
</Provider>
))
.add('Story #01', () => (
<Component {...props} />
));
(alpha) TESTING
import initStoryshots, { multiSnapshotWithOptions, imageSnapshot } from '@storybook/addon-storyshots';
initStoryshots({
suite: 'Structural Storyshots',
integrityOptions: { cwd: __dirname },
test: multiSnapshotWithOptions({})
});
initStoryshots({
suite: 'Image Storyshots',
test: imageSnapshot({ storybookUrl: 'http://localhost:9001', getMatchOptions }),
});
function getMatchOptions({ context: { fileName } }) {
const customSnapshotsDir = `${fileName.slice(0, fileName.lastIndexOf('/'))}/__image_snapshots__`;
return { customSnapshotsDir };
}
ALTERNATIVES
1. Storybook writes you stories in JavaScript files
2. Styleguidist writes you examples in Markdown files
3. Storybook shows one variation of component at a time
4. Styleguidist can show multiple component variations
5. Storybook is great for showing a component’s states
6. Styleguidist is useful for documentation and demos
APPLICATION
WHAT MATTERS
1. Create a new component (directories)
2. Import one module into another (proxy, clarity)
3. Jump to source (files)
4. Open a known file (exports)
5. Browse for a file I don’t know the name of (naming)
6. Change tab to another open file (index)
(best) PRACTICES
(some) PRACTICES
1. Use ES lint, guides, and ESLint React plugin
2. Use types propTypes, TypeScript or Flow
3. Know when to make new components
4. Component vs PureComponent vs Stateless
5. Use React (and Redux) Dev Tools
6. Use inline conditional statements in your code
7. Learn how React works with React internals
8. Improve development workflow (with Storybook)
(live) CODE REVIEW
1. Back in 2016: https://github.com/chvin/react-tetris
2. Now in 2019: https://github.com/skidding/flatris
REACT 16+
FIBER
- Scheduling:
- requestAnimationFrame
- requestIdleCallback - Prioritizing:
module.exports = {
NoWork: 0,
SynchronousPriority: 1, // user input
TaskPriority: 2, // hover
AnimationPriority: 3, // dimensions, sizes
HighPriority: 4, // scrolling
LowPriority: 5, // component re-rendering after new data from store arrived
OffscreenPriority: 6 // not visible elements
};
FRAGMENTS
function Glossary(props) {
return (
<dl>
{props.items.map(item => (
// Without the `key`, React will fire a key warning
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
);
}
ERROR BOUNDARIES
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
PORTALS
// Create portal
ReactDOM.createPortal(child, container);
// Render elements
render() {
// React mounts a new div and renders the children into it
return (
<div>
{this.props.children}
</div>
);
}
// Render portal
render() {
// React does *not* create a new div. It renders the children into `domNode`.
// `domNode` is any valid DOM node, regardless of its location in the DOM.
return ReactDOM.createPortal(
this.props.children,
domNode
);
}
CONTEXT
// React.createContext
const ThemeContext = React.createContext('light');
class ThemeProvider extends React.Component {
state = { theme: 'light' };
render() {
return (
// *Context.Provider
<ThemeContext.Provider value={this.state.theme}>
{ this.props.children }
</ThemeContext.Provider>
);
}
}
class ThemedButton extends React.Component {
render() {
return (
// *Context.Consumer
<ThemeContext.Consumer>
{ theme => <Button theme={theme} /> }
</ThemeContext.Consumer>
);
}
}
REF API
// React.createRef
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return <input type="text" ref={this.inputRef} />;
}
componentDidMount() {
this.inputRef.current.focus();
}
}
// React.forwardRef
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{ props.children }
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
LIFECYCLE
componentWillMount -> UNSAFE_*
componentWillReceiveProps ->UNSAFE_*
componentWillUpdate -> UNSAFE_*
+ getDerivedStateFromProps(*)
+ getDerivedStateFromError
+ getSnapshotBeforeUpdate
getDerivedStateFromProps
Anti-pattern #1: Unconditionally copying props to state
Anti-pattern #2: Erasing state when props change
class Component extends React.Component {
state = { externalData: null };
static getDerivedStateFromProps(props, state) {
// Store prevId in state so we can compare when props change.
// Clear out previously-loaded data (so we don't render stale stuff).
if (props.id !== state.prevId) {
return {
externalData: null,
prevId: props.id,
};
}
// No state update necessary
return null;
}
componentDidUpdate(prevProps, prevState) {
if (this.state.externalData === null) {
this._loadAsyncData(this.props.id);
}
}
}
STRICT MODE
import React from 'react';
function App() {
return (
<React.StrictMode>
<Component />
</React.StrictMode>
);
}
1. Identifying components with unsafe lifecycles
2. Warning about legacy string ref API usage
3. Warning about deprecated findDOMNode usage
4. Detecting unexpected side effects
5. Detecting legacy context API
LAZY MEMO
// React.memo
const MyComponent = React.memo(function MyComponent(props) {
/* only rerenders if props change */
});
// React.lazy
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function MyComponent() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
);
}
RENDER PROPS
class Cat extends React.Component {
render() {
const mouse = this.props.mouse;
return (
<img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
);
}
}
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>Move the mouse around!</h1>
<Mouse render={mouse => (
<Cat mouse={mouse} />
)}/>
</div>
);
}
}
vs HOCs
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
vs HOOKS (v16.8)
import { useState } from 'react';
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
6 RECIPES
Refining React
By Roman Stremedlovskyi
Refining React
6 Recipes from Experience in EPAM Systems
- 1,829