React для бэкэндеров

Валерий Кузьмин, Kontur.Recognition 2017

Что такое React

"A JavaScript library for building user interfaces"

Шаблоны

<div id="<%=Model.IdPrefix + Model.Data.Id%>" 
class="b-claim_list-item" 
>
...
</div>

ASPX

JQuery

<span class="b-lightbox-text-progress">
    {{if text}}
	${text}
    {{/if}}
    {{if textHtml}}
	{{html textHtml}}
    {{/if}}
</span>

Шаблоны

render(){
    return (
	<div className={styles.bodyText}> 
            <span className={styles.header}>
                Доступ запрещен
            </span>
	    <div className={styles.text}>
	        ...
	    </div> 
	</div>);
    } 

React (JSX)

Интерактив

<input
    type="text"
    data-mask="9.99"
    data-bind="editor:TextEditor,viewModel.metaViewModel.claimItem" 
/>

JsModels

claimItem: $m.str().requiredIf(null, function() {
    return !this.isForDeclaration();
}).regex(/^[1-2]\.\d{2}$/, "Имеет вид 1.ХХ (2.ХХ)"),
dd.Binder.createBindings(editorEl, editorDesc.getModel(this.model));

Интерактив

JsModels - Two-way binding (observe)

View

this.$el.bind("keydown cut paste", this.onChange.attach(this))
...
onChange:
model.value(this.$el.val())
model.value().onChange(setEditorValue)
...
setEditorValue: function(value)  {
    this.$el.val(value);
}

Smart

Model

Event

Event

Интерактив

JsModels - Two-way binding (CASCADE HELL)

View

Smart

Model

View

Smart

Model

Smart

Model

Интерактив

<Tooltip
    render={this.getHelpMessage}
>
    <Input
        mask="9.99"
        value={this.state.value || ''}
        onChange={this.handleChangeValue}
    />
</Tooltip>

React

getHelpMessage = () => {
    return this.state.isValid
        ? null
	: <div className={styles.helpMessage}>
	    <p>Пункт требования должен быть в формате:</p>
	    ...
	</div>;
};

Интерактив

React - One-way binding (declare)

View

Model

onChange={this.handleChangeValue}
...
handleChangeValue = (newValue) => this.setState({newValue})
<Input 
    value={this.state.value}
    ...
/>

Render

Event

<input value={this.state.value}/>

Интерактив

React - типичная ошибка

<input onChange={this.onChange}/>

или

Интерактив

React - One-way binding (declare)

View

State

(Model)

Reducer/Dispatcher

(Bus, Controller)

(newValue) => this.setState({newValue})

Интерактив

React - One-way binding (declare)

Интерактив

React - One-way binding (declare / flux)

Store + Dispatcher

Action

Интерактив

React - One-way binding (declare / proxy)

StorAGe

(Server)

Proxy

"Функциональный" подход

  • CSS1
  • CSS2
  • JS1
  • JS2
  • HTML1
  • HTML2

Компонентный подход

Component1

Component2

Структура компонента

// https://codesandbox.io/s/r0JAY3Gn4

import React from 'react';

export default class MyCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
    }
    increment () {
        this.setState({count: this.state.count + 1});
    }
    render() {
        return <div>
            <span>{this.state.count}</span>
            <button onClick={this.increment.bind(this)}>
                +1
            </button>
        </div>
    }
}

Props VS State

// https://codesandbox.io/s/g5B039BRZ

class MyCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
    }
    increment () {
        const newCount = this.state.count + 1;
        this.setState({count: newCount});
        this.props.onChange(newCount);
    }
    render() {
        return <div>
            <span>{this.state.count}</span>
            <button onClick={this.increment.bind(this)}>
                {this.props.buttonLabel}
            </button>
        </div>
    }
}

Props VS State

// https://codesandbox.io/s/g5B039BRZ

class MyApp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {appCount: 0};
  }
  onChange(newVal) {
    this.setState({appCount: newVal});
  }
  render() {
    return <div>App:{this.state.appCount} 
      <MyCounter 
        buttonLabel="Increment" 
        onChange={this.onChange.bind(this)}
      />
    </div>
  }
}

JSX

Примеры частично из https://habrahabr.ru/post/319270/

Пробовать на: https://babeljs.io/repl

Сахар для React.createElement

<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>
React.createElement(
  MyButton,
  { color: "blue", shadowSize: 2 },
  "Click Me"
);

Нотация через точку

<Modal.Header>
  Click Me
</Modal.Header>
React.createElement(
  Modal.Header,
  null,
  "Click Me"
);

Все кастомные - с большой,

все HTML - с маленькой

<Modal.Header>
  <div>Click Me</div>
</Modal.Header>
React.createElement(
  Modal.Header,
  null,
  React.createElement(
    "div",
    null,
    "Click Me"
  )
);

Все - expression (не statement!)

<MyComponent foo={"foo" + "bar"} />

<MyComponent foo="foobar" />

<MyComponent foo={'foobar'} />
React.createElement(MyComponent, { foo: "foo" + "bar" });

React.createElement(MyComponent, { foo: "foobar" });

React.createElement(MyComponent, { foo: 'foobar' });

Тонкости HTML - экранирование/деэкранирование

<MyComponent message={'<3'} />

<MyComponent message="<3" />
React.createElement(MyComponent, { message: "<3" });

<div>&lt;script&gt;alert(123)&lt;/script&gt;</div>

const App = ({prop}) => (<div>{prop}</div>)
<App prop="<script>alert(123)</script>" />

<MyComponent message="&lt;3" />

Тонкости HTML - если очень хочется...

function createMarkup() {
  return {__html: 'First <script>alert(123)</script> Second'};
}

function MyComponent() {
  return <div dangerouslySetInnerHTML={createMarkup()} />;
}

Аттрибуты, комменты

<div style={{background: 'red'}}/>

import './styles.less';
<div className="background-red"/>

import styles from './styles.less';
<div className={styles.backgroundRed}/>

import styles from './styles.less';
<div className={styles.backgroundRed}/>

<input type="checkbox" checked/>

var props = {claimId: 123};
<ServerStorage {...props}/>

<div>
{/* Hello world */}
</div>

Пустышки

<div></div>

<div>{false}</div>

<div>{null}</div>

<div>{true}</div>

Условия: if

<div>
   <h1>Hello!</h1>
   {
    unreadMessages.length > 0 &&
    <h2>
       You have {unreadMessages.length} unread messages.
     </h2>
   }
</div>

Условия: if-else

<div>
   <h1>Hello!</h1>
   {
    unreadMessages.length > 0 
    ?
        <h2>
           You have {unreadMessages.length} unread messages.
         </h2>
    :
        <span>No messages</span>    
   }
</div>

Babel

UBER-

Typescript-JSX

-STAGE-0

stage-0

ES2015

JSX

ES3

ES3Ify

.babelrc

{
  "presets": ["es2015", "stage-0", "react"]
}

export default class MyComponent extend React.Component {
    static classField = "456";
    instanceField = "123";
    boundFunc = () => {
        console.log(MyComponent.classField);
        MyComponent.classFunc();
        console.log(this.instanceField);
    };
    static classFunc = () => {
        console.log("class func");
    }
    render() {
        // вместо this.boundFunc.bind(this) или ::this.boundFunc
        return <button onClick={this.boundFunc}/>
    }
}

Важные фичи - классы

function ServerApi() {
    return Q($.getJSON('...')); // Promise
}

async function b() {
    try { // .then(result => console.log(result))
        const result = await externalLibApi();
        console.log(result);
    } 
    catch (e) { // .catch(e => console.error(e))
        console.error(e);
    }
    finally (e) { // .finally(() => { releaseResource() })
        releaseResource();
    }
}

b();

Важные фичи - async/await

// в массивах было [1, ...[2,3]] то же что [1, 2, 3];
// в функциях func(...[1,2])  то же что func(1,2)

var foo = {
    a: 1,
    b: 2,   
};

var bar = {
    a: 1,
    ...foo 
};
// вместо Object.assign или _.assign
// !!! shallow copy

const props = {onClick: ()=> someCall(), text: 'foo'}
<Button {...props}/>

const height = 100, width = 200;
<Button {...{height, width}}/>
// вместо <Button height={height} width={width}/>

Важные фичи - spread

// my-module-common.js
module.exports = {
    foo: function() {}
}
// main-common.js
const myModule = require('./my-module-common')
myModule.foo();

// my-module-es6.js
export default {
    foo: function() {}
}
// или
export function foo {} // или function foo() {}; export {foo};

// main-es6.js
import myModule from './my-module';
// или (автоматом исправляется babel)
const myModule = require('./my-module');
// НО! main-es6-to-common.js (gulp, тесты в mocha)
const myModule = require('./my-module').default;

Важные фичи - import

Webpack

JS(X)

LESS/CSS

JPG/PNG

TTF/WOFF(2)

babel-loader

style-loader

file-loader

bundle

(main.js)

webpack-dev-server + react-hot-loader

IIS

Import Document

dev-server

webpack

main.js

webpack-dev-server + react-hot-loader

render()

MyComponent

Change MyComponent.jsx!

main.js

hot-update.json

if (module.hot)
    module.hot.decline();

React внутри

Начало

root

ReactDOM.render(<App />, 
  document.getElementById('root'));

DOM

Начало

  • constructor(props)    
  • componentWillMount()    
  • render()    
  • componentDidMount()

this.setState

  • this.state = ... не работает
  • setState({foo: bar}) - частичное обновление
  • вызывает обновление
  • изменение асинхронно, может быть пакетным

this.setState

this.setState({foo: 'bar'})
/// ???
this.myCode(newState)
const newState = {...state, foo: 'bar'};
this.setState(newState)
this.myCode(newState)

Проблема:

Можно так:

this.setState

this.setState({foo: 'bar'})
/// ???
this.myCode(newState)
this.setState(
(prevState, props) => ({
        ...prevState
        foo: 'bar'
    }
)

Проблема:

Но лучше:

Обновление

  • componentWillReceiveProps(nextProps)
  • shouldComponentUpdate(nextProps, nextState)    
  • componentWillUpdate()    
  • render()    
  • componentDidUpdate(prevProps, prevState)

+ forceUpdate()

+componentWillUnmount()

Reconciliation

Text

Если тут есть state, он не будет заблокирован!

Reconciliation: keys

{_.map(arr, item => <Item key={shortid.generate()}/>)}
{_.map(arr, (item, i) => <Item key={i}/>)}
{_.map(arr, item => <Item key={item.idOrHash}/>)}

Плохо:

Так себе:

Норм:

Reconciliation: FOOO

Change!

State

Reconciliation: FOOO

SCU

SCU

Change!

State

Reconciliation: Redux

Change!

MagicState (Store)

React-паттерны

Сеть

componentDidMount() {
    this.fetchData();
}

fetchData() {
    this.setState({loading: true})
    api.requestData()
        .then((data) => this.setState({data}))
        .finally(() => this.setState({{loading: false}}))
}

render() {
    this.state.loading ? <Loader/> : <Data/>
}

Сеть: проблема

WARN: Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the App component.

WTF?

Сеть: решение

componentWillUnmount() {
    this.getAllUsersRequest.cancel();
}

CancellablePromise или Bluebird

Higher Order Component

Layout

Menu

???

Higher Order Component

render() {
    return (<div style={{padding: 10}}>
        {this.props.children}
    </div>);
}
import Layout from './Layout';
render() {
    return (<Layout>
        <Content/>
    </Layout>)
}

Layout.jsx

App.jsx

Higher Order Component

props.children == ReactElement || [ReactElement]

  • React.Children.map
  • React.Children.only

Stateless

class Layout {
    render() {
        const {children} = this.props;
        return (<div style={{padding: 10}}>
            {children}
        </div>);
    }
}

Layout.jsx

const Layout = ({children}) =>
     <div style={{padding: 10}}>{children}</div>;

LayoutV2.jsx

Stateful VS Stateless

State, no markup

Markup only

Stateful VS Stateless

Global state

Private state

+

markup

Events

Формы

Формы

render() {
    return (<Modal>
        <Input 
            value={this.state.email} 
            onChange={this.handleEmailChange}
        />
        <Input
            value={this.state.name} 
            onChange={this.handleNameChange}
        />
    </Modal>);
}

handleEmailChange = (val) => {this.setState({email: val})}
handleNameChange = (val) => {this.setState({name: val})}

Формы

render() {
    return (<Modal>
        <Editor 
            model={this.state}
            propName="email"
            onChange={this.handleChange}
        />
        <Editor
            model={this.state}
            propName="name"
            onChange={this.handleChange}
        />
    </Modal>);
}

handleChange = (propName, val) => {this.setState({[propName]: val})}

Формы

render() {
    const BoundEditor = ({propName}) => <Editor 
        propName={propName} model={this.state}
        onChange={this.handleChange}
    />
    return (<Modal>
        <BoundEditor propName="email" />
        <BoundEditor propName="name" />
    </Modal>);
}

handleChange = (propName, val) => {this.setState({[propName]: val})}

PropTypes

const Editor = ({propName, onChange, model, disabled, custom}) => 
    <Input value={model[propName]} {...{model, disabled, custom}}/>;

Editor.propTypes = {
    propName: React.PropTypes.string.isRequired,
    onChange: React.PropTypes.func.isRequired,
    model: React.PropTypes.object.isRequired // не очень
    disabled: React.PropTypes.bool,
    custom: CustomValidator,
}

Editor.defaultProps = {
    disabled: false,
    custom: null,
}

Imperative API

render() {
    return <Input ref={(ref) => this.input = ref}/>;   
}

onEventFromServer() {
    this.input.focus();
}

Recompose

import {withState, withHandlers, compose} from 'recompose';

const Counter = ({increment, decrement, count}) => 
    <div>{count}
        <button onClick={() => increment(count)}>+1</button>
        <button onClick={() => decrement(count)}>-1</button>
    </div>;

const enhance = compose(
    withState('count', 'setCount', 0),
    withHandlers({
        increment: ({setCount}) => (current) => setCount(current + 1),
        decrement: ({setCount}) => (current) => setCount(current - 1),
    })
)

export default enhance(Counter);

aka lodash for React

react-at-krec

By Valeriy Kuzmin

react-at-krec

  • 732