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><script>alert(123)</script></div>
const App = ({prop}) => (<div>{prop}</div>)
<App prop="<script>alert(123)</script>" />
<MyComponent message="<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
- 770