jinlong.github.com icon indicating copy to clipboard operation
jinlong.github.com copied to clipboard

React 进阶

Open jinlong opened this issue 6 years ago • 0 comments

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

jinlong avatar Sep 05 '18 09:09 jinlong