jinlong.github.com
jinlong.github.com copied to clipboard
React 进阶
JSX 条件渲染
示例代码
const sampleComponent = () => {
return (
<div>
{flag && flag2 && !flag3
? flag4
? <p>Blah</p>
: flag5
? <p>Meh</p>
: <p>Herp</p>
: <p>Derp</p>
}
</div>
)
};
如何改善?
- 最好的方法:逻辑移入子组件
- 讨巧的方法:利用 IIFE
const sampleComponent = () => {
const basicCondition = flag && flag2 && !flag3;
if (!basicCondition) return <p>Derp</p>;
if (flag4) return <p>Blah</p>;
if (flag5) return <p>Meh</p>;
return <p>Herp</p>
}
const sampleComponent = () => {
return (
<div>
{
(() => {
if (flag && flag2 && !flag3) {
if (flag4) {
return <p>Blah</p>
} else if (flag5) {
return <p>Meh</p>
} else {
return <p>Herp</p>
}
} else {
return <p>Derp</p>
}
})()
}
</div>
)
};
setState 异步特性
基于性能考虑,React 通常是批量合并更新,调用 setState() 之后,this.state 并没有马上修改,而是创建了一个中间态作为过渡。
但是有些例外情况,它是同步执行的,比如:eventListeners,Ajax,setTimeout 等。 原因是这些 JS 原生的 API 不在 React 的上下文控制范围,无法进行优化。
示例: http://jsbin.com/sezesaguba/edit?html,js,console,output
setState 之后马上取,通常获取的是旧值
console.log('State before : ' + JSON.stringify(this.state));
this.setState({
dollars: 50
});
console.log('State after : ' + JSON.stringify(this.state));
如何解决?
setState 第二个参数可以传入 callback
console.log('State before : ' + JSON.stringify(this.state));
this.setState({
dollars: 50
}, () => {
console.log('Here state will always be updated to latest version!');
console.log('State after : ' + JSON.stringify(this.state));
});
依赖注入
想象以下情况,title 属性需要从父级层层传递
<App>
<Header>
<Title>{ props.title }</Title>
</Header>
</App>
高阶组件可以实现依赖注入
// inject.jsx
var title = 'React Dependency Injection';
export default function inject(Component) {
return class Injector extends React.Component {
render() {
return (
<Component
{...this.state}
{...this.props}
title={ title }
/>
)
}
};
}
// Title.jsx
export default function Title(props) {
return <h1>{ props.title }</h1>;
}
// Header.jsx
import inject from './inject.jsx';
import Title from './Title.jsx';
var EnhancedTitle = inject(Title);
export default function Header() {
return (
<header>
<EnhancedTitle />
</header>
);
}
使用 React context
定义 context
var context = { title: 'React in patterns' };
class App extends React.Component {
getChildContext() {
return context;
}
// ...
}
App.childContextTypes = {
title: PropTypes.string
};
获取 context 值
class Inject extends React.Component {
render() {
var title = this.context.title;
// ...
}
}
Inject.contextTypes = {
title: PropTypes.string
};
展示和容器组件
考虑这种情况:数据和逻辑揉在一起
http://jsbin.com/guxokerubo/edit?html,js,output
解决方案
组件分离为两部分:容器和展示
容器组件处理数据和其他逻辑
// Clock/index.js
import Clock from './Clock.jsx'; // <-- that's the presentational component
export default class ClockContainer extends React.Component {
constructor(props) {
super(props);
this.state = {time: props.time};
this._update = this._updateTime.bind(this);
}
render() {
return <Clock { ...this._extract(this.state.time) }/>;
}
componentDidMount() {
this._interval = setInterval(this._update, 1000);
}
componentWillUnmount() {
clearInterval(this._interval);
}
_extract(time) {
return {
hours: time.getHours(),
minutes: time.getMinutes(),
seconds: time.getSeconds()
};
}
_updateTime() {
this.setState({time: new Date(this.state.time.getTime() + 1000)});
}
};
展示组件只负责展示,可以更换不同的展示组件
// Clock/Clock.jsx
export default function Clock(props) {
var [ hours, minutes, seconds ] = [
props.hours,
props.minutes,
props.seconds
].map(num => num < 10 ? '0' + num : num);
return <h1>{ hours } : { minutes } : { seconds }</h1>;
};
给 setState 传入 function
问题
// assuming this.state.count === 0
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
// this.state.count === 1, not 3
解决方案
this.setState((prevState, props) => ({
count: prevState.count + props.increment
}));
另一适用场景
// Passing object
this.setState({ expanded: !this.state.expanded });
// Passing function
this.setState(prevState => ({ expanded: !prevState.expanded }));
Switch 组件
转换组件:渲染多个组件中的一个
import HomePage from './HomePage.jsx';
import AboutPage from './AboutPage.jsx';
import UserPage from './UserPage.jsx';
import FourOhFourPage from './FourOhFourPage.jsx';
const PAGES = {
home: HomePage,
about: AboutPage,
user: UserPage
};
const Page = (props) => {
const Handler = PAGES[props.page] || FourOhFourPage;
return <Handler {...props} />
};
Page.propTypes = {
page: PropTypes.oneOf(Object.keys(PAGES)).isRequired
};
父组件访问子组件
比如 Autofocus 一个输入框
class Input extends Component {
focus() {
this.el.focus();
}
render() {
return (
<input
ref={el=> { this.el = el; }}
/>
);
}
}
父组件获取子组件的引用,调用它的方法
class SignInModal extends Component {
componentDidMount() {
this.InputComponent.focus();
}
render() {
return (
<div>
<label>User name:</label>
<Input
ref={comp => { this.InputComponent = comp; }}
/>
</div>
)
}
}
共享追踪逻辑
使用 HOC 共享统计追踪逻辑
- 符合 DRY (Do not Repeat Yourself) 原则
- 移除统计逻辑更容易
import tracker from './tracker.js';
// HOC
const pageLoadTracking = (ComposedComponent) => class HOC extends Component {
componentDidMount() {
tracker.trackPageLoad(this.props.trackingData);
}
componentDidUpdate() {
tracker.trackPageLoad(this.props.trackingData);
}
render() {
return <ComposedComponent {...this.props} />
}
};
// Usage
import LoginComponent from "./login";
const LoginWithTracking = pageLoadTracking(LoginComponent);
class SampleComponent extends Component {
render() {
const trackingData = {/** Nested Object **/};
return <LoginWithTracking trackingData={trackingData}/>
}
}
初始状态中的 Props
以下代码有何问题?
class SampleComponent extends Component {
// constructor function (or getInitialState)
constructor(props) {
super(props);
this.state = {
flag: false,
inputVal: props.inputValue
};
}
render() {
return <div>{this.state.inputVal && <AnotherComponent/>}</div>
}
}
如果组件更新,props 发生变化会怎样? constructor 和 getInitialState 仅在组件首次创建时执行
正确做法
class SampleComponent extends Component {
// constructor function (or getInitialState)
constructor(props) {
super(props);
this.state = {
flag: false
};
}
render() {
return <div>{this.props.inputValue && <AnotherComponent/>}</div>
}
}
复合组件与 Refs
class Field extends Component {
render() {
return (
<input type='text' ref={this.props.inputRef}/>
)
}
}
class MyComponent extends Component {
componentDidMount() {
this.inputNode.focus();
}
render() {
return (
<div>
Hello,
<Field inputRef={node => this.inputNode = node}/>
</div>
)
}
}
componentWillMount() 里面 setState()
componentWillMount 里面避免异步的初始化,放到 componentDidMount
componentWillMount 在 render 之前调用,在它里面 setState 不会引起重渲染
function componentDidMount() {
axios.get(`api/messages`)
.then((result) => {
const messages = result.data
console.log("COMPONENT WILL Mount messages : ", messages);
this.setState({
messages: [...messages.content]
})
})
}
包裹组件
父组件可以通过 this.props.children
属性获取传入的子组件
const SampleComponent = () => {
<Parent>
<Child />
</Parent>
};
const Parent = () => {
<div className="bla">
{this.props.children}
</div>
};
shouldComponentUpdate() 检查
使用 shouldComponentUpdate 避免昂贵的重渲染
export default class AutocompleteItem extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.url !== this.props.url ||
nextProps.selected !== this.props.selected;
}
render() {
const {props} = this;
const selectedClass = props.selected === true ? "selected" : "";
var path = parseUri(props.url).path;
path = path.length <= 0 ? props.url : "..." + path;
return (
<li
onMouseLeave={props.onMouseLeave}
className={selectedClass}>
<i className="ion-ios-eye"
data-image={props.image}
data-url={props.url}
data-title={props.title}
onClick={props.handlePlanetViewClick}/>
<span
onMouseEnter={props.onMouseEnter}>
<div className="dot bg-mint"/>
{path}
</span>
</li>
);
}
}
Synthetic events(合成事件)
React 里面事件对象都是被 SyntheticEvent 对象包裹的,为了提高性能,事件对象接收的处理器是与其他事件共用的
异步获取事件对象的属性是无法做到的
// wrong
function handleClick(event) {
setTimeout(function () {
console.log(event.target.name);
}, 1000);
}
// right
function handleClick(event) {
let name = event.target.name;
setTimeout(function () {
console.log(name);
}, 1000);
}
Clean code 整洁代码
DRY
self-commenting
命名可以做到见文知意
命名
变量名、方法名、文件名都要慎重考虑
- 布尔型的变量或者方法需要返回布尔值,以“is”,“has”,“should” 开头
// Dirty
const done = current >= goal;
// Clean
const isComplete = current >= goal;
- 方法命名需要指明它做什么,而不是如何做
// Dirty
const loadConfigFromServer = () => {
...
};
// Clean
const loadConfig = () => {
...
};
单一职责原则(single responsibility principle)
复杂的组件拆分成小组件,每一个组件只负责一种功能,并且把它做好
设置默认值
// Dirty
const Icon = ({ className, onClick }) => {
const additionalClasses = className || 'icon-large';
return (
<span
className={`icon-hover ${additionalClasses}`}
onClick={onClick}>
</span>
);
};
// Clean
const Icon = ({ className = 'icon-large', onClick }) => (
<span className={`icon-hover ${className}`} onClick={onClick} />
);
// Cleaner
const Icon = ({ className, onClick }) => (
<span className={`icon-hover ${className}`} onClick={onClick} />
);
Icon.defaultProps = {
className: 'icon-large',
};
使用无状态的 function 组件
const TableRowWrapper = ({ children }) => (
<tr>
{children}
</tr>
);
Rest/spread
const MyComponent = ({ className, ...others }) => (
<div className={className}>
<MyOtherComponent {...others} />
</div>
);
解构
对象解构
// Dirty
componentWillReceiveProps(newProps) {
this.setState({
active: newProps.active
});
}
// Clean
componentWillReceiveProps({ active }) {
this.setState({ active });
}
数组解构
// Dirty
const splitLocale = locale.split('-');
const language = splitLocale[0];
const country = splitLocale[1];
// Clean
const [language, country] = locale.split('-');
参考文章
- https://github.com/vasanthk/react-bits
- http://americanexpress.io/clean-code-dirty-code/
- https://github.com/enaqx/awesome-react