16.0
一、render
支持返回这五类:React elements, 数组,Fragments,Portal,String/numbers,boolean/null。
class Example extends React.Component {
render() {
return [
first element,
second element,
];
}
}
二、Error boundary(错误边界)
用于捕获子组件树的JS异常。
捕获范围:
- 渲染期间
- 生命周期内
- 整个组件树构造函数内
使用范围:
- 可以放在顶层,告诉用户有东西出错。但是这感觉失去了错误边界的意义。因为有一个组件出错了,其他正常的也没办法正常显示了
- 包在子组件外面,保护其他应用不崩溃。
// 先定一个组件ErrorBoundary
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, errorInfo: null };
}
componentDidCatch(error, errorInfo) {
// Catch errors in any components below and re-render with error message
this.setState({
error: error,
errorInfo: errorInfo
})
// You can also log error messages to an error reporting service here
}
render() {
// 有错误的时候展示回退
if (this.state.errorInfo) {
// Error path
return (
Something went wrong.
{this.state.error && this.state.error.toString()}
{this.state.errorInfo.componentStack}
);
}
// 正常的话,直接展示组件
return this.props.children;
}
}
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
throw new Error('I am crash');
}
handleClick() {
this.setState(({counter}) => ({
counter: counter + 1
}));
}
render() {
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error('I crashed!');
}
return {this.state.counter}
;
}
}
function App() {
return (
This is an example of error boundaries in React 16.
Click on the numbers to increase the counters.
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.
);
}
ReactDOM.render(
,
document.getElementById('root')
);
Error Boundary无法捕获下面的错误
- 事件函数里的错误
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = { error: null };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
try {
// Do something that could throw
} catch (error) {
this.setState({ error });
}
}
render() {
if (this.state.error) {
return Caught an error.
}
return Click Me
}
}
handleClick方法里面发生的错误,Error Boundary是捕获不到的。因为它不发生在渲染阶段,所以采用try/catch来捕获。
- 异步代码
(例如setTimeout 或 requestAnimationFrame 回调函数)
class A extends React.Component {
render() {
// 此错误无法被捕获,渲染时组件正常返回 ``
setTimeout(() => {
throw new Error('error')
}, 1000)
return (
)
}
}
服务端渲染
因为服务器渲染不支持Error BoundaryError Boundary自身抛出来的错误 (而不是其子组件)
三、react portal
Portal可以帮助我们在JSX中跟普通组件一样直接使用dialog, 但是又可以让dialog内容层级不在父组件内,而是显示在独立于原来app在外的同层级组件。
如何使用:
HTML:
// 这里为我们定义Dialog想要放入的位置
JS:
// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
// Let's create a Modal component that is an abstraction around
// the portal API.
class Modal extends React.Component {
constructor(props) {
super(props);
// Create a div that we'll render the modal into. Because each
// Modal component has its own element, we can render multiple
// modal components into the modal container.
this.el = document.createElement('div');
}
componentDidMount() {
// Append the element into the DOM on mount. We'll render
// into the modal container element (see the HTML tab).
// 这边会将我们生成的portal element插入到modal-root里。
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
// Remove the element from the DOM when we unmount
modalRoot.removeChild(this.el);
}
render() {
// Use a portal to render the children into the element
return ReactDOM.createPortal(
// Any valid React child: JSX, strings, arrays, etc.
this.props.children,
// A DOM element
this.el,
);
}
}
// The Modal component is a normal React component, so we can
// render it wherever we like without needing to know that it's
// implemented with portals.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {showModal: false};
this.handleShow = this.handleShow.bind(this);
this.handleHide = this.handleHide.bind(this);
}
handleShow() {
this.setState({showModal: true});
}
handleHide() {
this.setState({showModal: false});
}
render() {
// Show a Modal on click.
// (In a real app, don't forget to use ARIA attributes
// for accessibility!)
const modal = this.state.showModal ? (
//注意~~~~~~~~~~~~~这里可以自行加上这个调试
//
With a portal, we can render content into a different
part of the DOM, as if it were any other React child.
This is being rendered inside the #modal-container div.
//
) : null;
return (
This div has overflow: hidden.
{modal}
);
}
}
ReactDOM.render( , appRoot);
使用portal之后,modal不再嵌在app-root里。
四、自定义DOM属性
React 15:忽略未标准化的html 和 svg属性
React 16:去掉了这个限制
去除的原因是:
不能用自定义属性,对于非标准(proposal阶段)新属性还有其他框架(Angular)很不友好. React 15之所以可以过滤掉非标准的属性,是因为维护了一个白名单的文件(放在bundle size 里)。而随着时间的增加,标准化的属性越来越多,意味着要一直维护这个文件,同时这个文件也会越来越大,增加bundle的体积。
Hello, world!
五、优化SSR
- 生成更简洁的HTML
- 宽松的客户端一致性校验
- 无需提前编译
- react 16服务端渲染速度更快
- 支持流式渲染
1、生成更简洁的HTML
下面的HTML,react 15与react 16的服务端分别会生成什么。
renderToString(
This is some server-generated HTML.
);
react15:
有data-reactid, text noded ,react-text各种属性。
This is some
server-generated
HTML.
react 16:
增加易读性,同时很大程度上减少html的文件大小。
This is some server-generated
HTML.
2、宽松的客户端一致性校验
react 15:会将SSR的结果与客户端生成的做一个个字节的对比校验 ,一点不匹配发出warning同时就替换整个SSR生成的树。
react 16:对比校验会更宽松一些,比如,react 16允许属性顺序不一致,而且遇到不匹配的标签,还会做子树的修改,不是整个替换。
注意点: react16不会自动fix SSR 属性跟client html属性的不同,但是仍然会报warning,所以需要自己手动去修改。
3、无需提前编译
react 15:如果直接使用SSR,会有很多需要检查process.env的地方,但是读取在node中读取process.env是很消耗时间的。所以在react 15的时候,需要提前编译,这样就可以移除 process.env的引用。
react 16:只有一次检查process.env的地方,所以就不需要提前编译了,可以开箱即用。
4、react 16服务端渲染速度更快
react 15下,server client都需要生成vDOM,但是其实在服务端, 当使用renderToString的时候,生成的vDom就会被立即抛弃掉, 所以在server端生成vDom是没有意义的。
5、支持流式渲染 (renderyToNodeStream)
使用流式渲染会提升首个字节到(TTFB)的速度。
流式渲染可以理解为内容以一种流的形式传给前端。
所以在下一部分的内容被生成之前,开头的内容就已经被发到浏览器端了。
这样浏览器就可以更早的编译渲染文件内容。
// using Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
res.write("My Page ");
res.write("");
const stream = renderToNodeStream( );
stream.pipe(res, { end: false });
stream.on('end', () => {
res.write("");
res.end();
});
});
React 15:
// server:
// using Express client
import { renderToString } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
res.write("My Page ");
res.write("");
res.write(renderToString( ));
res.write("");
res.end();
});
// client
import { render } from "react-dom"
import MyPage from "./MyPage"
render( , document.getElementById("content"));
React 16:
其实就是把client端的render改成hydrate。
现在依然兼容render,但是17之后不再兼容,所以还是直接用hydrate好一点。
// client
import { hydrate } from "react-dom"
import MyPage from "./MyPage"
hydrate( , document.getElementById("content"));
注意事项:不支持ErrorBoundary 跟Portal,所以需要直出的页面就不能用了。
六、减小了32%bundle的体积
React 库大小从 20.7kb(压缩后 6.9kb)降低到 5.3kb(压缩后 2.2kb)
ReactDOM 库大小从 141kb(压缩后 42.9kb)降低到 103.7kb(压缩后 32.6kb)
React + ReactDOM 库大小从 161.7kb(压缩后 49.8kb)降低到 109kb(压缩后 43.8kb)
七、Fiber
React Fiber 初探
v16.1
react-call-return
v16.2
Fragement
React 15:render函数只能接受一个组件,所以一定要外层包一层
React16:可以通过Fragement直接返回多个组件。
Some text.
A heading
More text.
Another heading
Even more text.
React 15:
- 数组里的子节点必须要用逗号分离
- 数组里的子节点必须要带key防止warning
- string类型要用双引号
render() {
return [
"Some text.",
A heading
,
"More text.",
Another heading
,
"Even more text."
];
}
React16:
render() {
return (
// Extraneous div element :(
Some text.
A heading
More text.
Another heading
Even more text.
);
}
render() {
return (
<>
>
);
}
<> >
不支持写入属性,包括keys。如果需要key,可以直接使用
(但是Fragment目前也只接受key这一个属性,将来会支持更多)
function Glossary(props) {
return (
{props.items.map(item => (
// Without the `key`, React will fire a key warning
- {item.term}
- {item.description}
))}
);
}
16.3
一、新的生命周期函数
由于异步渲染的改动,componentWillMount
, componentWillReceiveProps
,componentWillUpdate
三个函数将被废弃。
由于这是一个很大的改变会影响很多现有的组件,所以需要慢慢的去改。
目前react 16 只是会报warning,在react 17就只能在前面加UNSAFE_
的前缀来使用。
可以使用rename-unsafe-lifecycles
辅助.
getDerivedStateFromProps
static getDerivedStateFromProps(props, state)
在调用render方法之前调用,无论是在初始安装还是后续更新。它应返回一个对象来更新状态,或者返回null以不更新任何内容。
根据props更新state
这个生命周期可用于替代componentWillReceiveProps
// Before
class ExampleComponent extends React.Component {
state = {
isScrollingDown: false,
};
componentWillReceiveProps(nextProps) {
if (this.props.currentRow !== nextProps.currentRow) {
this.setState({
isScrollingDown:
nextProps.currentRow > this.props.currentRow,
});
}
}
}
// After
class ExampleComponent extends React.Component {
// Initialize state in constructor,
// Or with a property initializer.
state = {
isScrollingDown: false,
lastRow: null,
};
static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow,
};
}
// Return null to indicate no change to state.
return null;
}
}
props更改时获取外部数据
// Before
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
componentDidMount() {
this._loadAsyncData(this.props.id);
}
componentWillReceiveProps(nextProps) {
if (nextProps.id !== this.props.id) {
this.setState({externalData: null});
this._loadAsyncData(nextProps.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = loadMyAsyncData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
getDerivedStateFromProps经常要配合componentDidUpdate使用
// After
class ExampleComponent 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;
}
componentDidMount() {
this._loadAsyncData(this.props.id);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.externalData === null) {
this._loadAsyncData(this.props.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// Render loading state ...
} else {
// Render real UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = loadMyAsyncData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
getSnapshotBeforeUpdate()
getSnapshotBeforeUpdate(prevProps, prevState)
在最近呈现的输出被提交到例如DOM之前调用。它使组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给componentDidUpdate()
。
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
{/* ...contents... */}
);
}
}
二、新的context API
context 就是可以使用全局的变量,不需要一层层传递props下去,比如主题颜色
// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
// Use a Provider to pass the current theme to the tree below.
// Any component can read it, no matter how deep it is.
// In this example, we're passing "dark" as the current value.
return (
);
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
return (
);
}
class ThemedButton extends React.Component {
// Assign a contextType to read the current theme context.
// React will find the closest theme Provider above and use its value.
// In this example, the current theme is "dark".
static contextType = ThemeContext;
render() {
return ;
}
}
一般来说,如果你只是想避免需要传很多次props的话,可以直接使用component composition(就是通过props自己传给指定的)会更好。
function Page(props) {
const user = props.user;
// 简单来说就是直接父组件将props传下去
const userLink = (
);
return ;
}
// Now, we have:
// ... which renders ...
// ... which renders ...
// ... which renders ...
{props.userLink}
context一般用于一些相同的data需要被大多的component用到,并且还是在不同的层级里。如主题,存储数据等。
三、createRef API
react15 提供了两种refs的方法: string 跟 callback:
//string:
class MyComponent extends React.Component {
constructor(props) {
super(props);
}
// 通过this.refs.textInput 来获取
render() {
return ;
}
}
//callback:
class MyComponent extends React.Component {
constructor(props) {
super(props);
}
// 通过this.textInput 来获取
render() {
return this.textInput = element} />;
}
}
string的用法可能会有问题,一般用callback
createRef
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return ;
}
componentDidMount() {
this.inputRef.current.focus();
}
}
注意事项:
1、functional component 是不能传ref属性的,因为他们没有instance
function MyFunctionComponent() {
return ;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// 这个不能工作
return (
);
}
}
但是只要引用的对象是DOM元素或者是class component, 那就可以在functional component里可以使用ref属性
function CustomTextInput(props) {
// textInput must be declared here so the ref can refer to it
let textInput = React.createRef();
function handleClick() {
textInput.current.focus();
}
return (
);
}
functional component里可以使用refs 但是不能把ref属性给它本身。
四、forwardRef API
父组件需要将自己的引用传给子组件
const TextInput = React.forwardRef((props, ref) => (
))
const inputRef = React.createRef();
class App extends Component {
constructor(props) {
super(props)
this.myRef = React.createRef()
}
handleSubmit = event => {
event.preventDefault()
alert('input value is:' + inputRef.current.value)
}
render() {
return (
)
}
}
const FancyButton = React.forwardRef((props, ref) => (
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
Click me! ;
这样就可以直接用this.ref 拿到对button的引用。
如果写的是一个高阶组件,推荐使用forwardAPI 将ref传给下面的component。
五、strictMode component
严格模式用来帮助开发者发现潜在问题的工具。就像Fragment 一样,它不会render任何的DOM 元素。注意:只有在development模式下才能用。
它可以帮助我们:
- 识别出使用不安全生命周期的组件
- 对使用string ref进行告警
- 对使用findDOMNode进行告警
- 探测某些产生副作用的方法
- 对使用弃用context API进行警告
- 还会有更多的功能在后续版本加进来。
function ExampleApplication() {
return (
);
}
v16.4
新增指针事件
新增了对对指针设备(例如鼠标,触控笔,或者手指触摸)触发的dom事件
onPointerDown
onPointerMove
onPointerUp
onPointerCancel
onGotPointerCapture
onLostPointerCapture
onPointerEnter
onPointerLeave
onPointerOver
onPointerOut
fix生命周期函数 - getDerivedStateFromProps
v16.5
React Profiler
React Developer Tools
v16.6
memo
React 15:如果想阻止组件的重复渲染,在class component里可以使用PureComponent, shouldComponentUpdate来优化。但是function component,没有这个功能, 只能每次都重新渲染。
React 16:为了全面拥抱function component,React团队写了memo来帮助function component实现这个阻止重复渲染的功能。
import React, { memo } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function Demo(props) {
console.log("render");
return {props.name};
}
// const Demo = memo(function Demo(props) {
// console.log("render");
// return {props.name};
// })
class App extends React.Component {
state = { count: 0 };
handleClick = () => {
this.setState({ count: 1 });
};
render() {
return (
Hello Memo
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render( , rootElement);
lazy、suspense
lazy需要跟Suspence配合使用。
lazy实际上是帮助我们实现代码分割的功能。
由于有些内容,并不一定要在首屏展示,所以这些资源没有必要一开始就要去获取,那么这些资源就可以动态获取。
这样的话,相当于把不需要首屏展示的代码分割出来,减少首屏代码的体积,提升性能。
Suspence 很像Error Boundary,不同的是Error Boundary是用来捕获错误,显示相应的callback组件。而Suspence是用来捕获还没有加载好的组件,并暂停渲染,显示相应的callback。
import React, { lazy, Suspense } from "react";
import ReactDOM from "react-dom";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import "react-tabs/style/react-tabs.css";
import "./styles.css";
import A from "./A";
// import B from "./B";
// 需要用到的时候才加载进来,当然还有预加载更好
const B = lazy(() => import("./B"));
import C from "./C";
function App() {
console.log(A);
console.log(B);
console.log(C);
return (
}>
需要注意:
SSR不支持lazy这个特性。
Lazy 必须搭配Suspence使用,否则会报错
简化static contextType
之前需要用一个在外层包裹一个
// Theme context, default to light theme
const ThemeContext = React.createContext('light');
// Signed-in user context
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// App component that provides initial context values
return (
);
}
}
function Layout() {
return (
);
}
// A component may consume multiple contexts
// 同时如果是function component 用Consumer
function Content() {
return (
{theme => (
{user => (
)}
)}
);
}
现在可以直接通过this.context
获取。
class MyClass extends React.Component {
static contextType = MyContext;
componentDidMount() {
let value = this.context;
/* perform a side-effect at mount using the value of MyContext */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* render something based on the value of MyContext */
}
}
MyClass.contextType = MyContext;
新增static getDerivedStateFromError
v16.3这个版本里,React 除了Error Boundaries来捕获错误,里面主要是使用了componentDidCatch来捕获 错误。但是它是在错误已经发生之后并且render函数被调用之后,才会被调用。 也就是说如果一个组件出现的错误,在调用 componentDidCatch之前只能返回null给用户。
而 getDerivedStateFromError 可以在render函数之嵌捕获到错误,所以它更适合写用来显示fallback UI的逻辑。
注意事项: componentDidCatch,getDerivedStateFromError都无法捕获服务端的错误,但是React团队正在努力支持SSR。
改进前的ErrorBoundary:
class ErrorBoundary extends React.Component {
state = { hasError: false };
componentDidCatch(error, info) {
this.setState({ hasError: false })
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return Something went wrong.
;
}
return this.props.children;
}
改进后的ErrorBoundary(推荐写法):
class ErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
// 更新state所以下次render可以立刻显示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 Something went wrong.
;
}
return this.props.children;
}