Skip to content

React 高级指引

第一章:面向组件编程高级

一、setState

1. 对象式 setState

在以前的认知中,setState 是用来更新状态的,一般给它传递一个对象,就像这样:

jsx
this.setState({
  count: count + 1
})

这样每次更新都会让 count 的值加 1。

这里做一个案例,点击点我按钮加 1,要在控制台输出每次的 count 的值。

如何实现呢?会考虑在 setState 更新之后 log 一下。

jsx
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 调用的第二个参数,可以接收一个函数,这个函数会在状态更新完毕并且界面更新之后调用。

jsx
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。

jsx
add = () => {
  this.setState((state) => ({ count: state.count + 1 }))
}

3. setState 同步与异步

1)旧说法

在此还需要注意的是,setState 有异步更新和同步更新两种形式,那么什么时候会同步更新,什么时候会异步更新呢?

React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事件,setTimeout/setInterval 等。

大部分开发中用到的都是 React 封装的事件,比如 onChange、onClick、onTouchMove 等,这些事件处理程序中的 setState 都是异步处理的。

jsx
// 创建组件
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,也就是并没有对其进行更新。这是因为异步的进行了处理,在输出的时候还没有对其进行处理。

jsx
componentDidMount(){
  document.getElementById("test").addEventListener("click",()=>{
    this.setState({isHot: this.state.isHot + 1});
    console.log(this.state.isHot);
  })
}

但是通过这个原生 JS,可以发现,控制台打印的就是 11,也就是已经对其进行了处理。也就是进行了同步的更新。

为什么 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。

jsx
// Parent 子组件
class Parent extends React.Component {
  state = {
    count: 0,
    name: 'parent'
  }

  // shouldComponentUpdate 接收两个参数:
  // nextProps - 接下来要更新的 props
  // nextState - 接下来要更新的 state
  shouldComponentUpdate(nextProps, nextState) {
    // 只有当 count 或 name 发生变化时才更新
    return (
      this.state.count !== nextState.count ||
      this.state.name !== nextState.name
    )
  }

  increment = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    console.log('Parent render')
    return (
      <div>
        <h2>Count: {this.state.count}</h2>
        <button onClick={this.increment}>+1</button>
        <Child name={this.state.name} />
      </div>
    )
  }
}

// Child 组件
class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    // 只有当传入的 name 属性发生变化时才更新
    return this.props.name !== nextProps.name
  }

  render() {
    console.log('Child render') 
    return <div>Child name: {this.props.name}</div>
  }
}

当点击按钮增加 count 时,只有 Parent 会重新渲染,Child 不会重新渲染。如果修改 name,两个组件都会重新渲染。

使用步骤

从 react 身上暴露出 PureComponent 而不使用 Component。

jsx
import React, { PureComponent } from 'react'

PureComponent 会对比当前和下一个状态的 prop 和 state,而这个比较属于浅比较,比较基本数据类型的值是否相同,而对于引用数据类型,比较的是它的引用地址是否相同,这个比较与内容无关

2. 高阶组件 memo

目前针对类组件可以使用 PureComponent,那么函数式组件呢?

事实上函数式组件我们在 props 没有改变时,也是不希望重新渲染其 DOM 树结构的。我们需要使用一个高阶组件 memo。

jsx
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 函数。lazy() 接收一个参数,这个参数必须是一个函数,该函数需要返回一个 Promise,且 Promise 需要 resolve 一个默认导出(default export)的 React 组件。

jsx
import React, { Component, lazy } from 'react';

然后需要更改引入组件的方式。采用 lazy 函数包裹。

jsx
const Home = lazy(() => import('./Home'))
const About = lazy(() => import('./About'))

会遇到这样的错误,提示要用一个标签包裹。

这里是因为当我们网速慢的时候,路由组件就会有可能加载不出来,页面就会白屏,它需要我们来指定一个路由组件加载的东西,相当于 loading。

jsx
import { Suspense } from 'react';

<Suspense fallback={<h2>数据加载中...</h2>}>
  <Route path="/home" component={Home}></Route>
  <Route path="/about" component={About}></Route>
</Suspense>

注意:因为 loading 是作为一个兜底的存在,因此 loading 是必须提前引入的,且不能懒加载。

4. Fragment

编写组件的时候每次都需要采用一个 div 标签包裹,才能让它正常的编译,但是这样会引发什么问题呢?最外层的标签是没有用的,只是要符合 jsx 语法。

可以采用 Fragment 来解决这个问题。

① 需要从 react 中暴露出 Fragment。

② 将所写的内容采用 Fragment 标签进行包裹,当它解析到 Fragment 标签的时候,就会把它去掉。

jsx
import React, { PureComponent, Fragment } from 'react'

export class App extends PureComponent {
  constructor() {
    super()

    this.state = {
      sections: [
        { title: "哈哈哈", content: "我是内容, 哈哈哈" },
        { title: "呵呵呵", content: "我是内容, 呵呵呵" },
        { title: "嘿嘿嘿", content: "我是内容, 嘿嘿嘿" },
        { title: "嘻嘻嘻", content: "我是内容, 嘻嘻嘻" },
      ]
    }
  }

  render() {
    const { sections } = this.state

    return (
      <>
        {
          sections.map(item => {
            return (
              <Fragment key={item.title}>
                <h2>{item.title}</h2>
                <p>{item.content}</p>
              </Fragment>
            )
          })
        }
      </>
    )
  }
}

export default App

语法糖:同时采用空标签(<></>),也能实现。但是它不能接收任何值,而 Fragment 能够有一个属性为 key 的属性,用于 diff 算法。

5. ErrorBoundary

当不可控因素导致数据不正常时,不能直接将报错页面呈现在用户的面前,由于没有办法给每一个组件、每一个文件添加判断,来确保正常运行,这样很不现实,因此要用到错误边界技术。

错误边界就是让这块组件报错的影响降到最小,不要影响到其他组件或者全局的正常运行。

例如 A 组件报错了,我们可以在 A 组件内添加一小段的提示,并把错误控制在 A 组件内,不影响其他组件。

实现

我们要对容易出错的组件的父组件做手脚,而不是组件本身。

在父组件中通过 getDerivedStateFromError 来配置子组件出错时的处理函数。方法应该返回一个对象,这个对象会被用来更新组件的 state。

jsx
// 当 Parent 的子组件出现报错时候,会触发 getDerivedStateFromError 调用,并携带错误信息
static getDerivedStateFromError(error) {
  console.log(error);
  return { hasError: error }
}

我们可以将 hasError 配置到状态当中,当 hasError 状态改变成 error 时,表明有错误发生,我们需要在组件中通过判断 hasError 值,来指定是否显示子组件。

jsx
{this.state.hasError ? <h2>出错啦</h2> : <Child />}

getDerivedStateFromError 只在生产环境中有效,开发环境看不到效果。

getDerivedStateFromError 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误。

componentDidCatch 是 React 组件生命周期中的一个方法,它在子组件树中的任何地方发生 JavaScript 错误时被调用。接收两个参数:

① error:抛出的错误。

② info:带有 componentStack 属性的对象。这个属性包含了错误发生时的组件堆栈信息。

可以在 componentDidCatch 中统计错误次数,通知编码人员进行 bug 解决。

jsx
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。

实际使用例子

jsx
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    console.log('Caught an error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 你可以自定义降级后的 UI 并渲染
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

// 使用方式
function App() {
  return (
    <ErrorBoundary>
      <MyComponent />
    </ErrorBoundary>
  );
}

ErrorBoundary 是一个错误边界组件。如果 MyComponent 或其子组件中抛出错误,componentDidCatch 会被调用。componentDidCatch 方法记录错误信息。在实际应用中,你可能会将这些信息发送到错误跟踪服务。

同时,getDerivedStateFromError 方法更新组件状态,触发重新渲染并显示降级 UI。在 render 方法中,如果 hasError 为 true,则显示错误消息;否则正常渲染子组件。

三、高阶组件

高阶组件的英文是 Higher Order Components,简称 HOC。

官方定义:高阶组件是参数为组件,返回值为新组件的函数。

解释:高阶组件本身不是一个组件,而是一个函数。其次,这个函数的参数是一个组件,返回值也是一个组件。

1. 内置高阶组件

1)Refs 转发

函数式组件是没有实例的,所以无法通过 ref 获取他们的实例。但是某些时候,可能想要获取函数式组件中的某个 DOM 元素。

这个时候可以通过 React.forwardRef 来获取。

jsx
import React, { forwardRef } from 'react';

const MyComponent = forwardRef((props, ref) => {
  return <div ref={ref}>Hello, world</div>;
});

export default MyComponent;

MyComponent 是一个函数组件,使用 React.forwardRef 来包装它。在父组件中这样使用 MyComponent:

jsx
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 元素了。

注意:React 19 中,不再需要使用 forwardRef。可以直接将 ref 作为一个 prop 传递。forwardRef 将在未来的版本中被弃用。

2)memo

前面说过。

2. 自定义高阶组件

应用一:props 的增强

需求:有多个 key-value 数据需要在某些组件内使用 props 获取到,如何优雅的实现?

1)定义高阶组件 @/hoc/enhanced_props.js

jsx
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 enhancedUserInfo

2)@/App.jsx 中使用

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.js

jsx
import { createContext } from "react"

const ThemeContext = createContext()

export default ThemeContext

2)编写高阶组件 @/hoc/with_theme.js

jsx
import ThemeContext from "@/context/theme_context"

function withTheme(OriginComponment) {
  return (props) => {
    return (
      <ThemeContext.Consumer>
        {
          value => {
            return <OriginComponment {...value} {...props}/>
          }
        }
      </ThemeContext.Consumer>
    )
  }
}

export default withTheme

3)@/App.jsx

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 App

4)在子组件 @/pages/Product.jsx 中使用

jsx
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.js

jsx
function loginAuth(OriginComponent) {
  return props => {
    // 从 localStorage 中获取 token
    const token = localStorage.getItem("token")

    if (token) {
      return <OriginComponent {...props}/>
    } else {
      return <h2>请先登录, 再进行跳转到对应的页面中</h2>
    }
  }
}

export default loginAuth

2)为 @/pages/Cart.jsx 组件添加登录鉴权

jsx
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 测试

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.js

jsx
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 logRenderTime

2)使用 @/pages/Detail.jsx

jsx
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 测试

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 节点中的不同位置也是有好处的。

jsx
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 的问题。

jsx
import React, { StrictMode } from 'react'

function App() {
  return (
    <StrictMode>
      {/* 界面结构代码 */}
    </StrictMode>
  );
}

export default App

StrictMode 组件包裹了应用的代码。这意味着 React 将检查 App 组件及其所有子组件的代码。

第二章:过渡动画

React 曾为开发者提供过动画插件 react-addons-css-transition-group,后由社区维护,形成了现在的 react-transition-group。

React Transition Group 专门用于在 React 应用中管理和处理过渡动画效果。这个库提供了一组组件,包括 Transition、CSSTransition、SwitchTransition 和 TransitionGroup,帮助在组件的进入和退出时应用动画效果。

  • Transition 是一个与平台无关的组件,通常结合 CSS 完成样式。
  • CSSTransition 是一个常用的组件,广泛用于添加过渡动画效果。它具有动画的作用时间(timeout)和指定元素首次渲染在页面时是否进行动画(appear)等参数。
  • SwitchTransition 用于在两个组件显示和隐藏切换时使用。
  • TransitionGroup 将多个动画组件包裹在其中,一般用于列表中元素的动画。

React Transition Group 可以应对大量常见简单动画,但如果需要编写高级动画,建议使用其他库如 react-springframer-motion 等。

安装

bash
# 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:在应用进入动画结束后被触发。
  • onExit:在进入离开动画之前被触发。
  • onExiting:执行离开动画时。
  • onExited:执行离开动画结束。

例子

jsx
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}
        appear
        onEnter={e => console.log("开始进入动画")}
      >
        <h2>哈哈哈</h2>
      </CSSTransition>
    </div>
  )

css
.appear, .why-enter {
  opacity: 0;
}

.appear-active, .why-enter-active {
  opacity: 1;
  transition: opacity 2s ease;
}

.why-exit {
  opacity: 1;
}

.why-exit-active {
  opacity: 0;
  transition: opacity 2s ease;
}

CSSTransition 在执行动画前需要拿到需执行动画元素的根元素,默认使用废弃的 findDOMNode 方法。可以通过给 CSSTransition 添加 nodeRef 属性告诉 CSSTransition 需执行动画元素的根元素,不要你自己去拿了。

jsx
export class App extends PureComponent {
  sectionRef = createRef()

  constructor(props) {
    // ...
  }

  render() {
    return (
      // ...
      <CSSTransition nodeRef={this.sectionRef}>
        <div ref={this.sectionRef}>
          <h2>哈哈哈</h2>
          <p>我是内容, 哈哈哈</p>
        </div>
      </CSSTransition>
    )
  }
}

二、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 属性。
jsx
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 (
      <>
        <SwitchTransition mode='out-in'>
          <CSSTransition
            key={isLogin ? "exit": "login"}
            classNames="login"
            timeout={1000}
          >
            <button onClick={e => this.setState({ isLogin: !isLogin })}>
              { isLogin ? "退出": "登录" }
            </button>
          </CSSTransition>
        </SwitchTransition>
      </>
    )
  }
}

export default App

style.css

css
.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

TransitionGroup 是 React Transition Group 库中的一个组件,用于管理一组需要进行动画过渡的子组件。它与 CSSTransition 或 Transition 组件配合使用,能够在组件进入、退出或改变状态时添加动画效果。

jsx
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

style.css

css
.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 方式

一、内联样式

jsx
style={{color: 变量1, fontSize: 变量2}}

变量 1 和变量 2 为 state 中的属性。

提示:别忘了 className 属性。

二、普通的 CSS

编写 .css 文件,在组件中使用 import 引入。缺点是样式会应用全局。

三、CSS Modules

CSS Modules 可以与预处理器(如 Sass 或 Less)一起使用,并且被许多现代 JavaScript 构建工具(如 Webpack 和 Parcel)支持。

首先,创建一个 CSS Module 文件。文件名通常以 .module.css 结尾,以区别于普通的 CSS 文件。例如,可以创建一个名为 Button.module.css 的文件:

css
/* Button.module.css */
.button {
  background-color: blue;
  color: white;
}

然后,在 React 组件中,就可以像导入 JavaScript 模块一样导入 CSS Module。然后,可以使用导入的样式对象来访问你的类名:

jsx
// 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 是以模块的形式导入的,你可以享受到模块化的好处,如更好的代码组织和更容易的重用。

但是这种方案也有自己的缺陷:

  • 引用的类名,不能使用连接符(.home-title),在 JavaScript 中是不识别的;
  • 所有的 className 都必须使用 {style.className} 的形式来编写;
  • 不方便动态来修改某些样式,依然需要使用内联样式的方式;

注意:要使用 CSS Modules,你可能需要配置你的构建工具(如 Webpack 或 Create React App)。在 Create React App 中,CSS Modules 是默认支持的,只需要按照上述方式命名你的 CSS 文件即可。

四、craco

五、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. 安装

bash
npm install --save styled-components

VSCode 有一款插件 vscode-styled-components 能识别 styled-components,并能自动进行 CSS 高亮、补全、纠正等。

2. 基础语法

1)基本样式组件

styled-components 创建的组件首字母必须以大写开头。几乎所有基础的 HTML 标签 styled 都支持。

styled.xxx 后面的 .xxx 代表的是最终解析后的标签,如果是 styled.a 那么解析出来就是 a 标签,styled.div 解析出来就是 div 标签。

注意:styled-components 会为我们自动创建 CSS 前缀。

jsx
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>
);

默认情况下,styled-components 生成的类名是纯哈希值(随机字符)。当安装并配置了 babel-plugin-styled-components 后,它会在编译阶段自动把组件的名字加到类名里。去参考 styled-components: Tooling 配一下即可。

bash
npm install --save-dev babel-plugin-styled-components

然后在 .babelrc 中配置:

json
{
  "plugins": ["babel-plugin-styled-components"]
}

注:如果使用的是 Next.js,现在通常只需要在 next.config.js 中开启 compiler: { styledComponents: true } 即可,不需要手动配 Babel。

2)动态样式

根据 props 动态改变组件的样式。

jsx
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>
  </>
);
3)样式继承

创建一个继承其它组件样式的新组件,最简单的方式就是用构造函数 styled() 包裹被继承的组件。

jsx
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 实现不传对应的属性则给一个默认值,如果传入对应的属性则使用传入的属性值。

jsx
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>
  );
}

attrs 也可以传入一个对象。

javascript
const Input = styled.input.attrs({
  // 直接指定一个值
  type: "text",

  // 给定一个默认值,可以传入 Props 进行修改
  size: (props => props.size) || "1em"
})`
  /* 在这里编写 css 样式 */
`
5)设置主题

styled-components 通过导出 <ThemeProvider> 组件从而能支持主题切换。<ThemeProvider> 是基于 React 的 Context API 实现的,可以为其下面的所有 React 组件提供一个主题。在渲染树中,任何层次的所有样式组件都可以访问提供的主题。

jsx
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 指定默认的主题 */}
{/* defaultProps 是 React 中的一个特性,用来为组件设置默认的 props 值。当父组件没有传入某个 prop 时,组件会使用 defaultProps 中定义的默认值 */}
Button.defaultProps = {
  theme: {
    main: "palevioletred"
  }
}

const theme = {
  main: "mediumseagreen"
};

render(
  <div>
    <Button>Normal</Button>
    {/* 采用了 ThemeProvider 提供的主题的 Button */}
    <ThemeProvider theme={theme}>
      <Button>Themed</Button>
    </ThemeProvider>
  </div>
);

效果:

  • 第一个使用默认主题(palevioletred)。
  • 第二个被 ThemeProvider 包裹,使用新定义的主题(mediumseagreen)。

3. React 中添加 class

Vue 中有 :class="{类名:布尔值, ......}"。而 React 中:

jsx
<div>
  <h2 className={"title " + (isActive ? "active": "")}>我是标题</h2>
  <h2 className={["title", (isActive ? "active": "")].join(" ")}>我是标题</h2>
</div>

非常麻烦。这个时候可以借助于一个第三方的库:JedWatson/classnames

jsx
import classNames from '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'

在 React 中使用:

jsx
const btnClass = classNames({
  btn: true,
  'btn-pressed': isPressed,
  'btn-over': !isPressed && isHovered,
});

<button
  className={btnClass}
>

<button
  className={classNames('foo', 'bar')}
>
preview
图片加载中
预览

Released under the MIT License.