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 个参数 preStatepreProps

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,也就是已经对其进行了处理。也就是进行了同步的更新。

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。

jsx
dem = () =>{
    this.setState({
        isHot: 1,
        cont: 444
    })
    this.setState({
    	isHot: this.state.isHot + 1

    })
    this.setState({
        isHot: 888,
        cont: 888
    })
}

上面的最后会输出:isHot 是 888,cont 是 888。

jsx
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

jsx
import React, { PureComponent } from 'react'

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

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 函数。

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

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

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

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

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

jsx
<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 来配置子组件出错时的处理函数。

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

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

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

在服务器中启动,才能正常看到效果。

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。

三、高阶组件

高阶组件的英文是 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 来包装它。现在,可以在父组件中通过 ref 访问到 MyComponent 中的 div 元素。

例如,在父组件中这样使用 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 元素。

2)memo

前面说过。

2. 自定义高阶组件

应用一:props 的增强

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

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

jsx
import { createContext } from "react"

const ThemeContext = createContext()

export default ThemeContext

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

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

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
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
应用四:生命周期

需求:计算某个组件从 componentWillMountcomponentDidMount 耗时多长时间?

1)编写高阶组件 @/hoc/log_render_time

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)使用

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

可以像下面这样使用 StrictMode

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

安装

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:在应用进入动画结束后被触发。
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}>
        <h2>哈哈哈</h2>
      </CSSTransition>
</div>
css
.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 属性。
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 (
      <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
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

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
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 中的属性。

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

注意:要使用 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. 安装

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

对于 react 开发者来说,这个还是比较香的。有人说用了这个之后,检查元素无法定位元素,其实它本身 name 是可以展示的,dev 开发时候有一个插件 styled-components: Tooling 配一下即可。

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>
  );
}
5)设置主题

styled-components 通过导出 <ThemeProvider> 组件从而能支持主题切换。<ThemeProvider> 是基于 React 的 Context APIopen in new window 实现的,可以为其下面的所有 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 指定默认的主题
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 中:

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

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

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

Released under the MIT License.