React 高级指引
第一章:面向组件编程高级
一、setState
1. 对象式 setState
在以前的认知中,setState 是用来更新状态的,一般给它传递一个对象,就像这样:
this.setState({
count: count + 1
})这样每次更新都会让 count 的值加 1。
这里做一个案例,点击点我按钮加 1,要在控制台输出每次的 count 的值。

如何实现呢?会考虑在 setState 更新之后 log 一下。
add = () => {
const { count } = this.state
this.setState({
count: count + 1
})
console.log(this.state.count);
}因此可能会写出这样的代码,看起来很合理,在调用完 setState 之后,输出 count。

发现显示的 count 和在控制台输出的 count 值是不一样的。
这是因为,调用的 setState 是同步事件,但是它的作用是让 React 去更新数据,而 React 不会立即的去更新数据,这是一个异步的任务,因此输出的 count 值会是状态更新之前的数据。“React 状态更新是异步的”。
那要如何实现同步呢?
其实在 setState 调用的第二个参数,可以接收一个函数,这个函数会在状态更新完毕并且界面更新之后调用。
add = () => {
const { count } = this.state
this.setState({
count: count + 1
}, () => {
console.log(this.state.count)
})
}将 setState 填上第二个参数,输出更新后的 count 值。

对象式的
setState是函数式setState的语法糖。
2. 函数式 setState
函数式的 setState 也是接收两个参数。
第一个参数是 updater,它是一个能够返回 stateChange 对象的函数。
第二个参数是一个回调函数,用于在状态更新完毕,界面也更新之后调用。
与对象式 setState 不同的是,传递的第一个参数 updater 可以接收到 2 个参数 preState 和 preProps。
add = () => {
this.setState((state) => ({ count: state.count + 1 }))
}
3. setState 同步与异步
1)旧说法
在此还需要注意的是,setState 有异步更新和同步更新两种形式,那么什么时候会同步更新,什么时候会异步更新呢?
React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事件,setTimeout/setInterval 等。
大部分开发中用到的都是 React 封装的事件,比如 onChange、onClick、onTouchMove 等,这些事件处理程序中的 setState 都是异步处理的。
// 创建组件
class St extends React.Component{
// 可以直接对其进行赋值
state = {isHot:10};
render(){ // 这个This也是实例对象
return <h1 onClick = {this.dem}>点击事件</h1>
}
// 箭头函数 [自定义方法--->要用赋值语句的形式+箭头函数]
dem = () =>{
// 修改isHot
this.setState({ isHot: this.state.isHot + 1})
console.log(this.state.isHot);
}
}上面的案例中预期 setState 使得 isHot 变成了 11,输出也应该是11。然而在控制台打印的却是 10,也就是并没有对其进行更新。这是因为异步的进行了处理,在输出的时候还没有对其进行处理。
componentDidMount(){
document.getElementById("test").addEventListener("click",()=>{
this.setState({isHot: this.state.isHot + 1});
console.log(this.state.isHot);
})
}但是通过这个原生 JS,可以发现,控制台打印的就是 11,也就是已经对其进行了处理。也就是进行了同步的更新。
React 怎么调用同步或者异步的呢?
在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新,而 isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state;但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true,而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdates,将 isBatchingUpdates 修改为 true,这样由 React 控制的事件处理过程 setState 不会同步更新 this.state。
如果是同步更新,每一个 setState 对调用一个 render,并且如果多次调用 setState 会以最后调用的为准,前面的将会作废;如果是异步更新,多个 setSate 会统一调用一次 render。
dem = () =>{
this.setState({
isHot: 1,
cont: 444
})
this.setState({
isHot: this.state.isHot + 1
})
this.setState({
isHot: 888,
cont: 888
})
}上面的最后会输出:isHot 是 888,cont 是 888。
dem = () =>{
this.setState({
isHot: this.state.isHot + 1
})
this.setState({
isHot: this.state.isHot + 1
})
this.setState({
isHot: this.state.isHot + 888
})
}初始 isHot 为 10,最后 isHot 输出为 898,也就是前面两个都没有执行。
注意:这是异步更新才有的,如果同步更新,每一次都会调用 render,这样每一次更新都会。
为什么 setState 设计为异步呢?
setState 设计为异步,可以显著的提升性能。
如果每次调用 setState 都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的。
最好的办法应该是获取到多个更新,之后进行批量更新。
如果同步更新了 state,但是还没有执行 render 函数,那么 state 和 props 不能保持同步。
state 和 props 不能保持一致性,会在开发中产生很多的问题。
2)新说法
New Feature: Automatic Batching

在 React18 之后:默认所有的操作都被放到了批处理中(异步处理)。
React18 之前:在组件生命周期或 React 合成事件中,setState 是异步;在 setTimeout 或者原生 dom 事件中,setState 是同步。
二、组件性能优化
1. PureComponent
问题引入
在之前一直写的代码中,我们一直使用的 Component 是有问题存在的。
① 只要执行 setState,即使不改变状态数据,组件也会调用 render。
② 当前组件状态更新,也会引起子组件 render。
而我们想要的是只有组件的 state 或者 props 数据发生改变的时候,再调用 render。
我们可以采用重写 shouldComponentUpdate (SCU) 的方法,但是这个方法不能根治这个问题,当状态很多时,我们没有办法增加判断。
那么,就可以采用 PureComponent。
使用步骤
从 react 身上暴露出 PureComponent 而不使用 Component。
import React, { PureComponent } from 'react'PureComponent 会对比当前对象和下一个状态的 prop 和 state,而这个比较属于浅比较,比较基本数据类型是否相同,而对于引用数据类型,比较的是它的引用地址是否相同,这个比较与内容无关。
2. 高阶组件 memo
目前针对类组件可以使用 PureComponent,那么函数式组件呢?
事实上函数式组件我们在 props 没有改变时,也是不希望其重新渲染其 DOM 树结构的。我们需要使用一个高阶组件 memo。
import React, { memo } from 'react';
const MyComponent = memo(function MyComponent(props) {
/* render using props */
return <div>{props.text}</div>;
});
export default MyComponent;MyComponent 是一个函数组件,使用 React.memo 来包装它。现在,只有当 props.text 改变时,MyComponent 才会重新渲染。如果 props.text 没有改变,即使父组件重新渲染,MyComponent 也不会重新渲染。
注意:React.memo 只比较 props 的浅层相等。
3. LazyLoad
懒加载在 React 中用的最多的就是路由组件了,页面刷新时,所有的页面都会重新加载。
如果有 100 个路由组件,但是用户只点击了几个,这就会有很大的消耗,因此需要做懒加载处理。用户点击哪个时,才去加载哪一个。
首先需要从 react 库中暴露一个 lazy 函数。
import React, { Component, lazy } from 'react';然后需要更改引入组件的方式。采用 lazy 函数包裹。
const Home = lazy(() => import('./Home'))
const About = lazy(() => import('./About'))会遇到这样的错误,提示要用一个标签包裹。

这里是因为,当我们网速慢的时候,路由组件就会有可能加载不出来,页面就会白屏,它需要我们来指定一个路由组件加载的东西,相对于 loading。
<Suspense fallback={<h1>loading</h1>}>
<Route path="/home" component={Home}></Route>
<Route path="/about" component={About}></Route>
</Suspense>注意:因为 loading 是作为一个兜底的存在,因此 loading 是必须提前引入的,且不能懒加载。
4. Fragment
编写组件的时候每次都需要采用一个 div 标签包裹,才能让它正常的编译,但是这样会引发什么问题呢?最外层的标签是没有用的,只是要符合 jsx 语法。
可以采用 Fragment 来解决这个问题。
① 需要从 react 中暴露出 Fragment。
② 将所写的内容采用 Fragment 标签进行包裹,当它解析到 Fragment 标签的时候,就会把它去掉。
同时采用空标签(
<></>),也能实现。但是它不能接收任何值,而Fragment能够有一个属性为key的属性,用于 diff 算法。
5. ErrorBoundary
当不可控因素导致数据不正常时,不能直接将报错页面呈现在用户的面前,由于没有办法给每一个组件、每一个文件添加判断,来确保正常运行,这样很不现实,因此要用到错误边界技术。
错误边界就是让这块组件报错的影响降到最小,不要影响到其他组件或者全局的正常运行。
例如 A 组件报错了,我们可以在 A 组件内添加一小段的提示,并把错误控制在 A 组件内,不影响其他组件。
我们要对容易出错的组件的父组件做手脚,而不是组件本身。
在父组件中通过 getDerivedStateFromError 来配置子组件出错时的处理函数。
// 当 Parent 的子组件出现报错时候,会触发 getDerivedStateFromError调用,并携带错误信息
static getDerivedStateFromError(error) {
console.log(error);
return { hasError: error }
}我们可以将 hasError 配置到状态当中,当 hasError 状态改变成 error 时,表明有错误发生,我们需要在组件中通过判断 hasError 值,来指定是否显示子组件。
{this.state.hasError ? <h2>出错啦</h2> : <Child />}在服务器中启动,才能正常看到效果。
componentDidCatch 是 React 组件生命周期中的一个方法,它在子组件树中的任何地方发生 JavaScript 错误时被调用。接收两个参数:
① error:抛出的错误。
② info:带有 componentStack 属性的对象。这个属性包含了错误发生时的组件堆栈信息。
可以在 componentDidCatch 中统计错误次数,通知编码人员进行 bug 解决。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// 在这里你可以记录错误或者显示回退 UI
this.setState({ hasError: true });
console.log(error, info);
}
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的回退 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}如果 ErrorBoundary 的子组件抛出错误,componentDidCatch 将被调用,错误和信息将被记录,然后组件将渲染一个回退 UI。
三、高阶组件
高阶组件的英文是 Higher Order Components,简称为 HOC。
官方的定义:高阶组件是参数为组件,返回值为新组件的函数。
解释:高阶组件本身不是一个组件,而是一个函数。其次,这个函数的参数是一个组件,返回值也是一个组件。
1. 内置高阶组件
1)Refs 转发
函数式组件是没有实例的,所以无法通过 ref 获取他们的实例。但是某些时候,可能想要获取函数式组件中的某个 DOM 元素。
这个时候可以通过 React.forwardRef 来获取。
import React, { forwardRef } from 'react';
const MyComponent = forwardRef((props, ref) => {
return <div ref={ref}>Hello, world</div>;
});
export default MyComponent;MyComponent 是一个函数组件,使用 React.forwardRef 来包装它。现在,可以在父组件中通过 ref 访问到 MyComponent 中的 div 元素。
例如,在父组件中这样使用 MyComponent:
import React, { useRef, useEffect } from 'react';
import MyComponent from './MyComponent';
function ParentComponent() {
const myRef = useRef();
useEffect(() => {
console.log(myRef.current); // 这将输出 div 元素
}, []);
return <MyComponent ref={myRef} />;
}在这个父组件中,创建了一个 ref,并将它传递给 MyComponent。然后,在 useEffect 中,就可以访问到 MyComponent 中的 div 元素。
2)memo
前面说过。
2. 自定义高阶组件
应用一:props 的增强
1)定义高阶组件 @/hoc/enhanced_props
import { PureComponent } from 'react'
// 定义组件: 给一些需要特殊数据的组件, 注入 props
function enhancedUserInfo(OriginComponent) {
class NewComponent extends PureComponent {
constructor(props) {
super(props)
this.state = {
userInfo: {
name: "yxts",
level: 999
}
}
}
render() {
return <OriginComponent {...this.props} {...this.state.userInfo}/>
}
}
return NewComponent
}
export default enhancedUserInfo2)@/App.jsx 中使用
import React, { PureComponent } from 'react'
import enhancedUserInfo from '@/hoc/enhanced_props'
const Home = enhancedUserInfo(function(props) {
return <h1>Home: {props.name}-{props.level}-{props.banners}</h1>
})
const Profile = enhancedUserInfo(function(props) {
return <h1>Profile: {props.name}-{props.level}</h1>
})
export class App extends PureComponent {
render() {
return (
<div>
<Home banners={["轮播1", "轮播2"]}/>
<Profile/>
</div>
)
}
}
export default App应用二:Context 共享
1)@/context/theme_context
import { createContext } from "react"
const ThemeContext = createContext()
export default ThemeContext2)编写高阶组件 @/hoc/with_theme
import ThemeContext from "@/context/theme_context"
function withTheme(OriginComponment) {
return (props) => {
return (
<ThemeContext.Consumer>
{
value => {
return <OriginComponment {...value} {...props}/>
}
}
</ThemeContext.Consumer>
)
}
}
export default withTheme3)@/App.jsx
import React, { PureComponent } from 'react'
import ThemeContext from '@/context/theme_context'
import Product from '@/pages/Product'
export class App extends PureComponent {
render() {
return (
<div>
<ThemeContext.Provider value={{color: "red", size: 30}}>
<Product/>
</ThemeContext.Provider>
</div>
)
}
}
export default App4)在子组件 @/pages/Product 中使用
import React, { PureComponent } from 'react'
import ThemeContext from '@/context/theme_context'
import withTheme from '@/hoc/with_theme'
export class Product extends PureComponent {
render() {
const { color, size } = this.props
return (
<div>
<h2>Product: {color}-{size}</h2>
</div>
)
}
}
export default withTheme(Product)应用三:登录鉴权
1)编写高阶组件 @/hoc/login_auth
function loginAuth(OriginComponent) {
return props => {
// 从 localStorage 中获取 token
const token = localStorage.getItem("token")
if (token) {
return <OriginComponent {...props}/>
} else {
return <h2>请先登录, 再进行跳转到对应的页面中</h2>
}
}
}
export default loginAuth2)为 @/pages/Cart 组件添加登录鉴权
import React, { PureComponent } from 'react'
import loginAuth from '@/hoc/login_auth'
export class Cart extends PureComponent {
render() {
return (
<h2>Cart Page</h2>
)
}
}
export default loginAuth(Cart)3)@/App.jsx 测试
import React, { PureComponent } from 'react'
import Cart from '@/pages/Cart'
export class App extends PureComponent {
loginClick() {
localStorage.setItem("token", "雨下田上")
this.forceUpdate()
}
render() {
return (
<div>
App
<button onClick={e => this.loginClick()}>登录</button>
<Cart/>
</div>
)
}
}
export default App应用四:生命周期
需求:计算某个组件从 componentWillMount 到 componentDidMount 耗时多长时间?
1)编写高阶组件 @/hoc/log_render_time
import { PureComponent } from "react";
function logRenderTime(OriginComponent) {
return class extends PureComponent {
UNSAFE_componentWillMount() {
this.beginTime = new Date().getTime()
}
componentDidMount() {
this.endTime = new Date().getTime()
const interval = this.endTime - this.beginTime
console.log(`当前${OriginComponent.name}页面花费了${interval}ms渲染完成!`)
}
render() {
return <OriginComponent {...this.props}/>
}
}
}
export default logRenderTime2)使用
import React, { PureComponent } from 'react'
import logRenderTime from '@/hoc/log_render_time'
export class Detail extends PureComponent {
render() {
return (
<div>
<h2>Detail Page</h2>
<ul>
<li>数据列表1</li>
<li>数据列表2</li>
<li>数据列表3</li>
</ul>
</div>
)
}
}
export default logRenderTime(Detail)3)@/App.jsx 测试
import React, { PureComponent } from 'react'
import Detail from '@/pages/Detail'
export class App extends PureComponent {
render() {
return (
<div>
<Detail/>
</div>
)
}
}
export default App四、其他
1. Portals
React Portal 是一种特殊的 API。某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的 DOM 元素中(默认都是挂载到 id 为 root 的 DOM 元素上的)。
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案:
- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。
- 第二个参数(container)是 一个 DOM 元素。
通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点。然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的。
import React from 'react';
import ReactDOM from 'react-dom';
class MyComponent extends React.Component {
render() {
// 将子元素渲染到 body 而不是当前 DOM 节点
return ReactDOM.createPortal(
this.props.children,
document.body
);
}
}
export default MyComponent;MyComponent 将其子元素渲染到 document.body,而不是其自身的 DOM 节点。这意味着,无论你在哪里使用 MyComponent,它的子元素总是会被渲染到 document.body。
2. StrictMode
StrictMode 是一个用于帮助检测潜在问题的 React 组件。它不会渲染任何可见的 UI,也不会影响生产环境的行为。它的主要目的是在开发过程中发现一些可能的问题。
StrictMode 目前可以帮助检测以下问题:
不安全的生命周期方法:某些生命周期方法在未来的 React 版本中将被弃用。当它们被使用时,
StrictMode会打印警告信息。使用过时或废弃的 API:当你使用过时或废弃的 API 时,
StrictMode会打印警告信息。意外的副作用:某些操作可能会导致组件的意外渲染。
StrictMode会帮助你发现这些问题。这个组件的 constructor 会被调用两次;
这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用。
在生产环境中,是不会被调用两次的。
使用废弃的 findDOMNode 方法:在严格模式下,使用
findDOMNode方法会触发警告。检测未预期的 context 使用:
StrictMode会警告你关于使用旧版 context API 的问题。
可以像下面这样使用 StrictMode:
import React from 'react';
function ExampleApplication() {
return (
<React.StrictMode>
<div>
{/* 你的应用代码 */}
</div>
</React.StrictMode>
);
}StrictMode 组件包裹了应用的其余部分。这意味着 React 将检查 ExampleApplication 组件及其所有子组件的代码。
第二章:过渡动画
React 曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。
React Transition Group 专门用于在 React 应用中管理和处理过渡动画效果。这个库提供了一组组件,包括 Transition、CSSTransition、SwitchTransition 和 TransitionGroup,帮助在组件的进入和退出时应用动画效果。
- Transition 是一个与平台无关的组件,通常结合 CSS 完成样式。
- CSSTransition 是一个常用的组件,广泛用于添加过渡动画效果。它具有动画的作用时间(timeout)和指定元素首次渲染在页面时是否进行动画(appear)等参数。
- SwitchTransition 用于在两个组件显示和隐藏切换时使用。
- TransitionGroup 将多个动画组件包裹在其中,一般用于列表中元素的动画。执行中有三个状态:appear,enter,exit,这需要定义对应的 CSS 样式。
React Transition Group 可以应对大量常见简单动画,但如果需要编写高级动画,建议使用其他库如 react-spring、framer-motion 等。
安装
# npm
npm install react-transition-group --save
# yarn
yarn add react-transition-group一、CSSTransition
CSSTransition 是基于 Transition 组件构建的。
三种状态
CSSTransition 执行过程中,有三个状态:appear、enter、exit。
它们有三种状态,需要定义对应的 CSS 样式:
- 第一类,开始状态:对于的类是 -appear、-enter、exit;
- 第二类:执行动画:对应的类是 -appear-active、-enter-active、-exit-active;
- 第三类:执行结束:对应的类是 -appear-done、-enter-done、-exit-done;
CSSTransition 常见对应的属性
- in:触发进入或者退出状态
- 当 in 为 true 时,触发进入状态。会添加 -enter、-enter-acitve 的 class 开始执行动画;当动画执行结束后,会移除两个 class,并且添加 -enter-done 的 class。
- 当 in 为 false 时,触发退出状态。会添加 -exit、-exit-active 的 class 开始执行动画;当动画执行结束后,会移除两个 class,并且添加 -enter-done 的 class。
- unmountOnExit:如果添加了
unmountOnExit={true},那么该组件会在执行退出动画结束后被移除掉。
- classNames:动画 class 的名称
- 决定了在编写 css 时,对应的 class 名称。比如 card-enter、card-enter-active、card-enter-done。
- timeout:
- 过渡动画的时间。
- appear:
- 是否在初次进入添加动画(需要和 in 同时为 true)。
其他属性可以参考官网来学习:React Transition Group。
钩子函数
CSSTransition 对应的钩子函数:主要为了检测动画的执行过程,来完成一些 JavaScript 的操作。
- onEnter:在进入动画之前被触发。
- onEntering:在应用进入动画时被触发。
- onEntered:在应用进入动画结束后被触发。
render(){
const { isShow } = this.state
return (
<div>
<button onClick={e => this.setState({isShow: !isShow})}>切换</button>
{/* { isShow && <h2>哈哈哈</h2> } */}
<CSSTransition in={isShow} unmountOnExit={true} classNames="why" timeout={2000}>
<h2>哈哈哈</h2>
</CSSTransition>
</div>.why-enter {
opacity: 0;
}
.why-enter-active {
opacity: 1;
transition: opacity 2s ease;
}
.why-exit {
opacity:1;
}
.why-exit-active {
opacity: 0;
transition: opacity 2s ease;
}二、SwitchTransition
SwitchTransition 可以完成两个组件之间切换的炫酷动画:
比如我们有一个按钮需要在 on 和 off 之间切换,我们希望看到 on 先从左侧退出,off 再从右侧进入。
这个动画在vue中被称之为 vue transition modes。
react-transition-group中使用SwitchTransition来实现该动画。
SwitchTransition中主要有一个属性:mode,有两个值。
- in-out:表示新组件先进入,旧组件再移除。
- out-in:表示就组件先移除,新组建再进入。
如何使用SwitchTransition呢?
- SwitchTransition 组件里面要有 CSSTransition 或者 Transition 组件,不能直接包裹你想要切换的组件。
- SwitchTransition 里面的 CSSTransition 或 Transition 组件不再像以前那样接受 in 属性来判断元素是何种状态,取而代之的是 key 属性。
import React, { PureComponent } from 'react'
import { SwitchTransition, CSSTransition } from 'react-transition-group'
import "./style.css"
export class App extends PureComponent {
constructor() {
super()
this.state = {
isLogin: true
}
}
render() {
const { isLogin } = this.state
return (
<div>
<SwitchTransition mode='out-in'>
<CSSTransition
key={isLogin ? "exit": "login"}
classNames="login"
timeout={1000}
>
<button onClick={e => this.setState({ isLogin: !isLogin })}>
{ isLogin ? "退出": "登录" }
</button>
</CSSTransition>
</SwitchTransition>
</div>
)
}
}
export default App.login-enter {
transform: translateX(100px);
opacity: 0;
}
.login-enter-active {
transform: translateX(0);
opacity: 1;
transition: all 1s ease;
}
.login-exit {
transform: translateX(0);
opacity: 1;
}
.login-exit-active {
transform: translateX(-100px);
opacity: 0;
transition: all 1s ease;
}三、TransitionGroup
import React, { PureComponent } from 'react'
import { TransitionGroup, CSSTransition } from "react-transition-group"
import "./style.css"
export class App extends PureComponent {
constructor() {
super()
this.state = {
books: [
{ id: 111, name: "你不知道JS", price: 99 },
{ id: 222, name: "JS高级程序设计", price: 88 },
{ id: 333, name: "Vuejs高级设计", price: 77 },
]
}
}
addNewBook() {
const books = [...this.state.books]
books.push({ id: new Date().getTime(), name: "React高级程序设计", price: 99 })
this.setState({ books })
}
removeBook(index) {
const books = [...this.state.books]
books.splice(index, 1)
this.setState({ books })
}
render() {
const { books } = this.state
return (
<div>
<h2>书籍列表:</h2>
<TransitionGroup component="ul">
{
books.map((item, index) => {
return (
<CSSTransition key={item.id} classNames="book" timeout={1000}>
<li>
<span>{item.name}-{item.price}</span>
<button onClick={e => this.removeBook(index)}>删除</button>
</li>
</CSSTransition>
)
})
}
</TransitionGroup>
<button onClick={e => this.addNewBook()}>添加新书籍</button>
</div>
)
}
}
export default App.book-enter {
transform: translateX(150px);
opacity: 0;
}
.book-enter-active {
transform: translateX(0);
opacity: 1;
transition: all 1s ease;
}
.book-exit {
transform: translateX(0);
opacity: 1;
}
.book-exit-active {
transform: translateX(150px);
opacity: 0;
transition: all 1s ease;
}第三章:React 的 CSS 方式
一、内联样式
style={{color: 变量1, fontSize: 变量2}}变量 1 和变量 2 为 state 中的属性。
二、普通的 CSS
编写 .css 文件,在组件中使用 import 引入。缺点是样式会应用全局。
三、CSS Modules
CSS Modules 可以与预处理器(如 Sass 或 Less)一起使用,并且被许多现代 JavaScript 构建工具(如 Webpack 和 Parcel)支持。
首先,创建一个 CSS Module 文件。文件名通常以 .module.css 结尾,以区别于普通的 CSS 文件。例如,可以创建一个名为 Button.module.css 的文件:
/* Button.module.css */
.button {
background-color: blue;
color: white;
}然后,在 React 组件中,就可以像导入 JavaScript 模块一样导入 CSS Module。然后,可以使用导入的样式对象来访问你的类名:
// Button.js
import React from 'react';
import styles from './Button.module.css';
const Button = () => (
<button className={styles.button}>Click me</button>
);
export default Button;这种方式可以确保你的 CSS 类名在组件之间是隔离的,避免了类名冲突。同时,由于 CSS 是以模块的形式导入的,你可以享受到模块化的好处,如更好的代码组织和更容易的重用。
注意:要使用 CSS Modules,你可能需要配置你的构建工具(如 Webpack 或 Create React App)。在 Create React App 中,CSS Modules 是默认支持的,只需要按照上述方式命名你的 CSS 文件即可。
四、CSS in JS
CSS-in-JS 就是将应用的 CSS 样式写在 JavaScript 文件里面,而不是独立为一些 .css、.scss 或者 .less 之类的文件,这样你就可以在 CSS 中使用一些属于 JS 的诸如模块声明、变量定义、函数调用和条件判断等语言特性来提供灵活的可扩展的样式定义。
使用这个技术写的库有很多(emotion、glamorous),在 react 中最火的就是 styled-components。vue 中 css scope 也是这个思想,每个组件都有它的 scopeId,样式进行绑定,css modules 也是同样的。react 中 css in js 为什么火,框架本身就是 html css js 写在一个组件混着写,虽然有些违背一些主流说法,但这就是它的特点,毕竟本身就就可以说 html in js,再来一个 css in js 也很正常。
1. 安装
npm install --save styled-componentsVSCode 有一款插件 vscode-styled-components 能识别 styled-components,并能自动进行 CSS 高亮、补全、纠正等。

2. 基础语法
1)基本样式组件
styled-components 创建的组件首字母必须以大写开头。几乎所有基础的 HTML 标签 styled 都支持。
styled.xxx 后面的 .xxx 代表的是最终解析后的标签,如果是 styled.a 那么解析出来就是 a 标签,styled.div 解析出来就是 div 标签。
styled-components 会为我们自动创建 CSS 前缀。
import styled from 'styled-components'
/*
创建一个Title组件,
将render一个带有样式的h1标签
*/
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
/*
创建一个Wrapper组件,
将render一个带有样式的section标签
*/
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
// 使用 Title and Wrapper 得到下面效果图
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);
2)动态样式
根据 props 动态改变组件的样式。
const Button = styled.button`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
render(
<>
<Button>Normal</Button>
<Button primary>Primary</Button>
</>
);对于 react 开发者来说,这个还是比较香的。有人说用了这个之后,检查元素无法定位元素,其实它本身 name 是可以展示的,dev 开发时候有一个插件 styled-components: Tooling 配一下即可。

3)样式继承
创建一个继承其它组件样式的新组件,最简单的方式就是用构造函数 styled() 包裹被继承的组件。
const Button = styled.button`
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
// 一个继承 Button 的新组件, 重载了一部分样式
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;
render(
<div>
<Button>Normal Button</Button>
<TomatoButton>Tomato Button</TomatoButton>
</div>
);
4)attrs
在一些 HTML 标签中是有一些属性的。比如 input 标签中,有 type 这个属性,我们就可以使用 attrs 实现不传对应的属性则给一个默认值,如果传入对应的属性则使用传入的属性值。
import styled from "styled-components";
const Input = styled.input.attrs((props) => ({
// 直接指定一个值
type: "text",
// 给定一个默认值,可以传入Props进行修改
size: props.size || "1em"
}))`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
margin: ${(props) => props.size};
padding: ${(props) => props.size};
`;
export default function App() {
return (
<div>
<Input placeholder="A small text input" />
<br />
<Input placeholder="A bigger text input" size="2em" />
</div>
);
}5)设置主题
styled-components 通过导出 <ThemeProvider> 组件从而能支持主题切换。<ThemeProvider> 是基于 React 的 Context APIopen in new window 实现的,可以为其下面的所有 React 组件提供一个主题。在渲染树中,任何层次的所有样式组件都可以访问提供的主题。
import styled, {ThemeProvider} from "styled-components";
// 通过使用 props.theme 可以访问到 ThemeProvider 传递下来的对象
const Button = styled.button`
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border-radius: 3px;
color: ${props => props.theme.main};
border: 2px solid ${props => props.theme.main};
`;
// 为 Button 指定默认的主题
Button.defaultProps = {
theme: {
main: "palevioletred"
}
}
const theme = {
main: "mediumseagreen"
};
render(
<div>
<Button>Normal</Button>
// 采用了 ThemeProvider 提供的主题的 Button
<ThemeProvider theme={theme}>
<Button>Themed</Button>
</ThemeProvider>
</div>
);3. React 中添加 class
Vue 中有 :class="{类名:布尔值, ......}"。而 React 中:
<div>
<h2 className={"title " + (isActive ? "active": "")}>我是标题</h2>
<h2 className={["title", (isActive ? "active": "")].join(" ")}>我是标题</h2>
</div>非常麻烦。这个时候可以借助于一个第三方的库:JedWatson/classnames
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
// 各种类型的大量参数
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
// 其他的假值都会被忽略
classNames(null, false, 'bar', undefined, 0, { baz: null }, ''); // => 'bar'