Vue视角万字总结React

2,384 阅读15分钟

前言

最近 B 站复习 react 整理笔记。全文大部分示例以 vue 为第一视角分析使用方式。总结肯定有不到位或者漏掉的地方。毕竟还是以实用为主。一些不常用的 API 就没有演示。

也希望能在不同视角下带领小伙伴们加深对 react 的印象。

React

state

Vuedata 功能一致。使用上有些许不同。stateReact 组件的实例属性。与之对应的也有一个实例方法 setState 用来修改数据。但需要注意的是 setState 千万不要修改原数据。

import React from 'react';

class App extends React.Component {
  constructor() {
    super();
    // 定义数据的 state
    this.state = {
      count: 0
    };
  }
  render() {
    return (
      <>
        <div>{state.count}</div>
        {/* 修改时使用 setState */}
        <button onClick={() => this.setState({ count: count + 1 })}>increment</button>
      </>
    );
  }
}

属性

在 React 中属性的传递同样是 propsVue 基本一致。

import React from 'react';

class Navbar extends React.Component {
  state = {
    username: 'zhangsan',
    age: 24
  };
  render() {
    return (
      <>
        {/* 既可以传递单个也可以解构传递多个 */}
        <NavbarChild type="text" name={this.state.username} {...this.state} />
      </>
    );
  }
}
// NavbarChild
class NavbarChild extends React.Component {
  render() {
    // 子组件中则通过 this.props 获取父组件中传递过来的属性对象
    console.log(this.props);
  }
}

属性校验

通过内部提供的 prop-types 校验库,以及两个属性帮助用户校验 prop 以及格式。相比较 vue 的校验 prop 略显麻烦。

类组件属性校验 通过静态属性 propTypesdefaultProps 来进行校验以及默认值

import React from 'react';
// 内部提供的校验 props 的库。
import { bool, string } from 'prop-types';

export default class Navbar extends React.Component {
  // 默认值
  static defaultProps = {
    isShow: false
  };
  // 属性校验使用静态属性
  static propTypes = {
    isShow: bool,
    title: string
  };

  render() {
    const { isShow, title } = this.props;
    return (
      <>
        <div>{title}</div>
        {isShow && <span>显示内容</span>}
      </>
    );
  }
}

函数式组件类型校验 使用同样的属性,挂载到函数上面

import { string, number } from 'prop-types';

// 函数式组件的props是通过参数进行接收
function Sidebar(props) {
  const { username, age } = props;
  return (
    <ul>
      <li>{username}</li>
      <li>{age}</li>
    </ul>
  );
}
// 类型校验
Sidebar.propTypes = {
  username: string,
  age: number
};
// 默认值
Sidebar.defaultProps = {
  username: 'zhangsan',
  age: 25
};

事件

对比 vue 从使用层面看事件只是写法上的不同。React 中的事件以 onXxxx 比如 onChange, onInput 等等。但是在使用组件时需要注意 this指向 以及 params 传递的问题

import React from 'react';

class App extends React.Component {
  change() {}

  render() {
    return (
      <>
        {/* 这种调用方式需要加上 bind 方式 this指向出现问题 */}
        <input type="text" onChange={this.change.bind(this)} />
        {/* 如果不想加 bind 可以使用箭头函数但是要手动调用这个函数,因为事件会执行箭头函数并没有执行事件函数 */}
        <input type="text" onChange={() => this.change()} />
        {/* 第二种方式也可以接受参数 */}
        <input type="text" onChange={event => this.change(event)} />
        {/* 如果函数比较简单也可以考虑直接写在行内 */}
        <input
          type="text"
          onChange={event => {
            console.log(event.target.value);
          }}
        />
      </>
    );
  }
}

组件通信

子传父

严格意义应该是 子组件修改父组件传递过来的值,不管是 Vue 还是 React 都不支持用户直接在子组件中去修改父组件的值。Vue 使用 emit 暴露一个事件。而 React 则让父组件传递一个回调函数给子组件,让子组件调用。

import React from 'react';

export default class MenuList extends React.Component {
  state = {
    isShow: false
  }
  // 修改属性
  toggleShow() {
    this.setState({
      isShow: !this.state.isShow
    })
  }

  render() {
    return (
      <div>
        {/** 传递给子组件 */}
        {/** <Navbar toggleShow={this.toggleShow.bind(this)} />*/}

        {/** 获取子组件传递的参数 方便后续操作 */}
        <Navbar
          toggleShow={params => {
            this.toggleShow();
          }}
        />
        <div>menu</div>
      </div>
    );
  }
}

class Navbar extends React.Component {
  render() {
    // 子组件决定何时调用方法更改属性。
    // return <button onClick={this.props.toggleShow}>toggle</button>;

    // 如果需要传递使用下面方式
    return (
      <button
        onClick={() => {
          // 子组件在调用时传递参数
          this.props.toggleShow('params');
        }}
      >
        toggleShow
      </button>
    );
  }
}

父传子

通过 props 进行传递,于 vue 类似。

import React from 'react';
// Parent
export default class Parent extends React.Component {
  render() {
    const info = { username: 'zhangsan', age: 24 };
    // 可以传递单个也可以展开进行传递
    return <Child msg="xx" status={true} {...info} />;
  }
}
// Child
class Child extends React.Component {
  render() {
    // this.props 接收父组件传递进来的所有属性。可以解构
    console.log(this.props);
    return <div></div>;
  }
}
// 如果是函数式组件props将是一个参数
function Child(props) {
  console.log(props);
}

跨组件通信

于 Vue 中的 provide inject 相似,在需要注入数据的地方使用,后续在这个组件内的所有组件都可以享受同样的数据。

// App.jsx
import React from 'react';
// 创建上下文对象
export const GlobalContext = React.createContext();
export default class App extends React.Component {
  state = {
    username: 'zhangsan',
    age: 24
  };

  render() {
    return (
      // value 是要注入的内容 注意:不要注入普通对象,可能会造成更新了数据但是没有更新视图
      <GlobalContext.Provider value={this.state}>
        <Child />
      </GlobalContext.Provider>
    );
  }
}
// Child.jsx
import React from 'react';
import GroundSon from './GroundSon';
import { GlobalContext } from './App';

export default class App extends React.Component {
  state = {
    username: 'zhangsan',
    age: 24,
    changeUsername: username => {
      this.setState({ username });
    }
  };

  render() {
    return (
      <>
        <GlobalContext.Consumer>
          {/* consumer 需要返回一个函数,这个函数的参数就是 Provider 注入的数据 */}
          {parentState => (
            <>
              <div>username: {parentState.username}</div>
              <div>age: {parentState.age}</div>
            </>
          )}
        </GlobalContext.Consumer>
        {/* 孙子组件 */}
        <GroundSon />
      </>
    );
  }
}
// GroundSon.jsx
import React from 'react';
import { GlobalContext } from './App';

export default class App extends React.Component {
  state = {
    username: 'zhangsan',
    age: 24
  };

  render() {
    return (
      <>
        <GlobalContext.Consumer>
          {/* consumer 需要返回一个函数,这个函数的参数就是 Provider 注入的数据 */}
          {parentState => (
              <button onClick={() => parentState.changeUsername('lisi')}>changeUsername</button>
          )}
        </GlobalContext.Consumer>
      </>
    );
  }
}

v-html 平替

import React from 'react';

export default class Markdown extends React.Component {
  render() {
    // 接收字符串形式的 html,解析为 html 代码
    return <div dangerouslySetInnerHTML={{ __html: '<div>hello world</div>' }}></div>;
  }
}

$refs 平替 createRef

import React from 'react';

export default class Input extends React.Component {
  // this.inputRef.current 访问到的是原生 DOM 对象
  inputRef = React.createRef();

  render() {
    return <input type="text" ref={this.inputRef} />;
  }
}

teleport 平替 createPortal

import React, { useState } from 'react';
// 注意这个方法 在 react-dom 中
import { createPortal } from 'react-dom';

export default function App() {
  const [show, setShow] = useState(false);

  return (
    <>
      <button onClick={() => setShow(!show)}>toggle</button>
      {show && <Dialog close={() => setShow(!show)} clear={() => setVal('')} />}
    </>
  );
}
function Dialog(props) {
  // 创建元素到哪个位置
  return createPortal(
    <>
      <button onClick={props.clear}>清空</button>
      <button onClick={props.close}>取消</button>
    </>,
    document.body
  );
}

defineAsyncComponent 和 异步 setup 的平替解决方案 React.lazySuspense

import React, { useState, Suspense } from 'react';

const Home = React.lazy(() => import('./components/Home'));
const About = React.lazy(() => import('./components/About'));

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>toggleComponent</button>
      <Suspense fallback={<>loading...</>}>
        {show ? <Home /> : <About />}
      </Suspense>
    </>
  );
}

防止组件重新渲染 PureComponent memo

react 中当组件更新时,默认情况下不管有没有更新数据,组件都会重新渲染。可以使用 PureComponentmemo 来进行优化,让没有涉及到更新的组件不进行二次渲染。以上两个 API 针对 React 中组件书写的两种方式。

import React, { Component, memo, PureComponent, useState } from 'react';

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>toggleComponent</button>
      {/* 如果往子组件传递内容后,是一定会触发更新的 */}
      <ClassChild show={show} />
      <FuncChild />
    </>
  );
}

// class ClassChild extends Component {  // 父组件只是更新状态也会导致该组件的重新render
class ClassChild extends PureComponent {
  // 如果使用 PureComponent 之后此组件只会在开始的时候渲染一次后续不会渲染。
  render() {
    console.log('render class component');
    return <div>Child</div>;
  }
}

// function FuncChild() { // 父组件只是更新状态也会导致该组件的重新render
const FuncChild = memo(() => {
  // 如果使用 memo 之后此组件只会在开始的时候渲染一次后续不会渲染。
  console.log('render functional component');
  return <div>Child</div>;
});

受控组件 非受控组件

受控组件于 Vue 中的 v-model 类似。通过自己维护一个状态 state,然后侦听触发的事件 onChange。当触发数据更新时再通过 setState 更改数据。 非受控组件只是在初始化的时候,定义初始值,随后就把控制权交出去。

除了以上特性,对于使用层面也是有一定的区别。

更多关于受控于非受控组件的参考:受控和非受控组件真的那么难理解吗?在实际业务中如何灵活运用受控组件与非受控组件

条件受控组件非受控组件
提交时校验支持支持
只使用一次或不会变的数据支持支持
有条件禁用提交按钮支持不支持
强制输入格式支持不支持
一条数据的多个输入支持不支持
动态表单支持不支持
import React from 'react';

export default class Navbar extends React.Component {
  constructor() {
    super();
    this.inputRef = React.createRef();
    this.state = {
      value: 'hello'
    };
  }

  render() {
    return (
      <div>
        {/* 非受控组件 */}
        <input type="text" defaultValue={'hello'} ref={this.inputRef} />
        <button
          onClick={() => console.log(this.inputRef.current.value)}
          // 如果在非受控组件中使用动态校验禁用btn 会报错。
          // disabled={this.inputRef.current.value === 'helloWorld'}
        >
          Get
        </button>
        <button onClick={() => (this.inputRef.current.value = '')}>Clean</button>
        <br />
        {/* 受控组件 */}
        <input
          type="text"
          value={this.state.value}
          onChange={event => {
            this.setState({ value: event.target.value });
          }}
        />
        <button onClick={() => console.log(this.state.value)} disabled={this.state.value === 'helloWorld'}>
          Get
        </button>
      </div>
    );
  }
}

生命周期

React 的生命周期函数只会存在于类组件中。 图解,在 React 的生命周期中很多人会把 constructor 当做第一个钩子,但其实不然。 constructor 是类在被实例化时必须要走的一个函数,下面就不计算在内了。

挂载时生命周期

React 组件在挂载时,会触发 render -> componentDidMount

import React, { Component } from 'react';

class Child extends Component {
  render() {
    console.log('render - 渲染函数');
    return <>child</>;
  }

  componentDidMount() {
    console.log('componentDidMount - 组件挂载完成');
  }
}

更新时生命周期

React 组件在更新时,会触发 render -> componentDidUpdate

import React, { Component } from 'react';

class Child extends Component {
  constructor() {
    super();
    this.state = {
      count: 0
    };
  }

  render() {
    console.log('render - 渲染函数');
    return (
      <>
        <div>{this.state.count}</div>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>updateState</button>
        <div>{this.props.count}</div>
        <button onClick={this.props.updateProps}>updateProps</button>
      </>
    );
  }
  /**
   * snapshot: 后续在进行讲解
   * prevProps, prevState 组件更新前的 props 和 state
   * */
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('componentDidUpdate - 组件更新完成');
    console.log('prevProps: ', prevProps, 'prevState:', prevState, 'snapshot: ', snapshot);
  }
}

export default class App extends Component {
  state = {
    count: 0
  };
  render() {
    return (
      <div>
        <Child count={this.state.count} updateProps={() => this.setState({ count: this.state.count + 1 })} />
      </div>
    );
  }
}

在更新时还有其他的钩子,这里单独拎出来方便理解。

shouldComponentUpdate

此钩子在 render 之前调用,返回 boolean 用于控制是否渲染组件。本身也会接收最新的 propsstate

import React, { Component } from 'react';

class Child extends React.Component {
  state = {
    username: 'zhangsan'
  };

  /**
   * @description 是否允许组件更新 执行时机类似于 vue 的 beforeUpdate,但是职责却不相同
   * @param {*} nextState
   * @param {*} nextProps
   * @returns boolean
   */
  shouldComponentUpdate(nextProps, nextState) {
    console.log('shouldComponentUpdate - 是否允许组件更新');
    // 通过判断 username 是否相同,返回 boolean 确认是否让组件重新 render
    if (this.state.username === nextState.username) {
      return false;
    }
    return true;
  }

  render() {
    console.log('render - 渲染函数');
    return (
      <>
        <div>{this.state.username}</div>
        {/* 每次点击都会触发render,理论上如果每次都是 lisi 那么后续其实没必要触发render */}
        <button onClick={() => this.setState({ username: 'lisi' })}>changeState</button>
      </>
    );
  }
}
getSnapshotBeforeUpdate

此钩子在 render 之后调用。必须返回一个值不能为 undefined,返回值会作为 componentDidUpdate 的第三个参数,注意这个钩子必须和 componentDidUpdate 同时出现

需求:当列表增加高度后,仍然定位到没有增加高度之前的位置。

import React, { Component } from 'react';

class Child extends React.Component {
  state = {
    list: [1, 2, 3, 4, 5, 6, 7, 8, 9],
    insertList: [11, 22, 33, 44, 55, 66, 77, 88, 99]
  };
  wraperRef = React.createRef();

  /**
   * @description 获取更新的快照 执行时机类似于 vue 的 beforeUpdate,但是职责却不相同
   * @param {*} nextState
   * @param {*} nextProps
   * @returns any
   */
  getSnapshotBeforeUpdate(nextProps, nextState) {
    console.log('getSnapshotBeforeUpdate', 'nextProps: ', nextProps, 'nextState', nextState);

    // 返回更新之前的高度
    return this.wraperRef.current.scrollHeight;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('componentDidUpdate');
    // 获取更新后的高度
    const scrollHeight = this.wraperRef.current.scrollHeight;

    // 设置滚动条的高度
    this.wraperRef.current.scrollTop += scrollHeight - snapshot;
  }

  render() {
    return (
      <>
        {/* 假设需要点击之后需要让滚动条记住更新前的位置则可以使用 getSnapshotBeforeUpdate */}
        <button onClick={() => this.setState({ list: [...this.state.insertList, ...this.state.list] })}>insert</button>
        <div style={{ height: '200px', overflow: 'auto' }} ref={this.wraperRef}>
          {this.state.list.map(item => {
            return (
              <div
                key={item}
                style={{
                  lineHeight: '50px',
                  backgroundColor: item % 2 === 0 ? 'skyblue' : 'hotpink'
                }}
              >
                {item}
              </div>
            );
          })}
        </div>
      </>
    );
  }
}

卸载时生命周期

import React, { Component } from 'react';

class Child extends Component {
  componentWillUnmount() {
    console.log('componentWillUnmount');
  }

  render() {
    return <div>我是子组件</div>;
  }
}

export default class App extends Component {
  state = {
    isShow: true
  };
  render() {
    return (
      <>
        <button onClick={() => this.setState({ isShow: !this.state.isShow })}>isShow</button>
        {this.state.isShow && <Child />}
      </>
    );
  }
}

挂载和更新都会被触发的钩子

除了 render 之外 React 还提供一个静态 钩子在挂载和更新之前都会被执行。getDerivedStateFromProps 该方法接收新的 props 和 老的 state 并返回一个对象。这个对象会合并组件的 state

import React, { Component } from 'react';

class Child extends Component {
  state = { username: 'zhangsan' };

  static getDerivedStateFromProps(nextProps, prevState) {
    console.log('getDerivedStateFromProps', 'nextProps: ', nextProps, 'prevState', prevState);
    return {
      age: 24
    };
  }
  render() {
    return (
      <>
        <button onClick={() => this.setState({})}>forceUpdate</button>
        <div>{JSON.stringify(this.state)}</div>
      </>
    );
  }
}

总结

React 组件创建的过程中会调用 getDerivedStateFromProps -> render -> componentDidMount

React 组件更新的过程中会调用 getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate

React 组件卸载的过程中会调用 componentWillUnmount

其中 getDerivedStateFromProps 接收两个参数分别是 nextProps prevState 并需要返回一个对象

shouldComponentUpdate 接收两个参数分别是 nextProps nextState 并需要返回一个 boolean

getSnapshotBeforeUpdate 接收两个参数分别是 prevProps prevState 并需要返回一个值且不能是 undefined

componentDidUpdate 接收三个参数分别是 nextProps nextState snapshot

Hooks

React 16.8 之后出现的 api 拓展了函数式组件的能力。

useState

类似于 Vue 中的 ref reactive 作用一致,并不代表使用方式相同。

import { useState } from 'React';

function App() {
  // useState 返回两个参数,一个是值,一个是修改值的函数。
  const [username, setUsername] = useState('lisi');
  return (
    <button onClick={() => setUsername('zhangsan')}>changeState</button>
    <div>{username}</div>
  );
}

useEffect

VuewatchEffect 功能基本一致,但是在使用层面有些许差别。

export default function App() {
  const [count, setCount] = useState(0);
  /**
   * @param fn 副作用函数
   * @param array 依赖数据,当依赖数据为空数组时,只会在挂载的时候执行一次,当数组中有值时,会根据值的变化来执行副作用函数
   */
  useEffect(() => {
    // 此时count每次更改都会触发该副作用
    console.log('count change');
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>add</button>
      <div>{count}</div>
    </div>
  );
}

众所周知 VuewatchEffect 想要清除副作用可以在副作用函数中传递一个 onCleanUp 参数,在 React 中则需要在副作用函数中在 return 一个函数,在这个 return 的函数中清除副作用。

import React, { useEffect, useState } from 'react';

function Child() {

  function resizeHandler() {
    console.log('resize');
  }
  useEffect(() => {
    window.addEventListener('resize', resizeHandler);

    // 此时当 Child 被卸载时则会调用此函数。
    return () => {
      window.removeEventListener('resize', resizeHandler);
    };
  }, []);
}

export default function App() {
  const [isShow, setIsShow] = useState(true);
  return (
    <>
      <button onClick={() => setIsShow(!isShow)}>isShow</button>
      {isShow && <Child />}
    </>
  );
}

useLayoutEffect

在 DOM 更新之后渲染之前调用。于 useEffect 相似的一个 hook,作用几乎一直,推荐优先使用 useEffect,加入需要在页面挂载之后做存粹的 DOM 操作则可以使用该 hook,在这里操作 DOM 之后才会进入渲染页面的阶段,减少不必要的回流和重绘。

useCallback

在函数式组件中,只要设置了 state 整个函数就会被执行。函数内部定义的一些工具函数为了防止重新创建可以使用此 hook 进行缓存。

import React, { useCallback, useState } from 'react';

const stack = [];
const equalStack = [];

// 五秒内点击一次isShow 收集函数创建时的递增、递减函数
// 如果 useCallback 真的缓存了,那第一次和第二次肯定是相等的,否则不然。
setTimeout(() => {
  console.log('useCallback', equalStack[0] === equalStack[1]);
  console.log(stack[0] === stack[1]);
}, 5000);

// 首次渲染时创建一次
export default function App() {
  console.log('function create');

  const [count, setCount] = useState(0);
  const [show, setShow] = useState(true);

  const decrease = () => {
    setCount(count - 1);
  };
  const increment = useCallback(() => {
    setCount(count + 1);
  }, [count]);

  equalStack.push(increment);
  stack.push(decrease);

  return (
    <>
      <div>{count}</div>
      <button onClick={increment}>increment</button>
      <button onClick={decrease}>decrease</button>
      {/* 当点击 isShow 时函数肯定会被创建 同时上面的两个函数一个被缓存,一个被创建 */}
      <button onClick={() => setShow(!show)}>isShow</button>
      {show && <Child />}
    </>
  );
}

const Child = () => <div>Child</div>

useMemo

类似于 Vue 中的 computed 让依赖的值发生变化时会执行并产生结果返回,与之不同的是 useMemo 需要手动的声明依赖的值。

import React, { useMemo, useState } from 'react';

export default function App() {
  const [count, setCount] = useState(0);

  // 首次渲染时执行一次,往后当依赖项count的值发生改变的时候则会重新执行
  const doubleCount = useMemo(() => count * 2, [count]);

  return (
    <fieldset>
      <legend>countValue</legend>
      <div>count: {count}</div>
      <div>doubleCount: {doubleCount}</div>
      <button onClick={() => setCount(count + 1)}>increment</button>
    </fieldset>
  );
}

useRef

于类组件中的 React.createRef() 功能相同,使用也是类似。

import React, { useRef, useState } from 'react';

export default function App() {
  const [inputVal, setInputVal] = useState('');

  // 函数式组件中不再是 React.createRef() 而是 useRef()
  const inputRef = useRef();
  return (
    <div>
      <input
        type="text"
        ref={inputRef}
        value={inputVal}
        onChange={() => {
          // 访问时 DOM 元素同样是在 current 上面
          setInputVal(inputRef.current.value);
        }}
      />
    </div>
  );
}

useContext

函数式组件的跨组件通信解决方案。相比较类组件而言,可以不用再书写 GlobalContext.Consumer 组件了。

import React, { useState, useContext } from 'react';

const GlobalContext = React.createContext();

export default function App() {
  const [info, setInfo] = useState({ username: 'zhangsan', age: 24 });
  return (
    <GlobalContext.Provider value={{ info, setInfo }}>
      <Son />
    </GlobalContext.Provider>
  );
}

function Son() {
  // 子组件只需要使用 useContext 即可获得父组件传入进来的内容
  const { info } = useContext(GlobalContext);
  return (
    <div>
      <div>username: {info.username}</div>
      <div>userage: {info.age}</div>
      <GroundSon />
    </div>
  );
}

function GroundSon() {
  const { setInfo } = useContext(GlobalContext);
  return <button onClick={() => setInfo({ username: 'lisi', age: 25 })}>setUserInfo</button>;
}

useReducer

小型的 VueStore 或者 Pinia

import React, { useReducer } from 'react';

/**
 * @description reducer 一定要是一个纯函数(修改内容的标准与类组件的中 setState 以及函数式组件中的 useState 一致,不要直接更改原数据)。并且不支持异步。
 * @param {*} prevState 被触发当时的 state { count: xxx }
 * @param {*} action dispatch 传递进来的参数 { type: 'xxx' }
 * @returns state
 */
const reducer = (prevState, action) => {
  const newState = { ...prevState };
  switch (action.type) {
    case 'increment':
      newState.count++;
      return newState;
    case 'decrease':
      newState.count--;
      return newState;
    default:
      return prevState;
  }
};
const initState = {
  count: 0
};

export default function App() {
  // state 是用户定义的状态,dispatch 用来派发事件。
  const [state, dispatch] = useReducer(reducer, initState);

  return (
    <>
      <button onClick={() => dispatch({ type: 'decrease' })}>decrease</button>
      <div>count: {state.count}</div>
      <button onClick={() => dispatch({ type: 'increment' })}>increment</button>
    </>
  );
}

自定义 Hooks

告诉了解 Vue3 的你,几乎一模一样

import React, { useState } from 'react';

function useCounter() {
  const [count, setCount] = useState(0);
  function increment() {
    setCount(count + 1);
  }
  function decrease() {
    setCount(count - 1);
  }
  return {
    count,
    increment,
    decrease
  };
}

export default function App() {
  const { count, increment, decrease } = useCounter();
  return (
    <div>
      <button onClick={decrease}>decrease</button>
      <div>count: {count}</div>
      <button onClick={increment}>increment</button>
    </div>
  );
}

CSS

React 中可以书写 css 的方式有以下几种。

  • 行内样式 直接在 jsx 中书写

  • 外部资源 通过导入的方式书写,但是可能会出现样式冲突。为此 create-react-app 提供一种解决方案,在文件后面添加 .module。 比如 button.module.css 此时编译出来的 css 会加上 hash 值,防止冲突。

import style from './style.css';

function App() {
  return <div className={style.container}></div>
}
  • styled-components 一种可以将样式直接写入 .jsx 的方案;使用前需要先进行安装 文档
import styled from 'styled-components';

function App() {
  const Div = styled.div`
    width: 20px;
    height: 20px;
    background-color: `${props => props.primary ? 'skyblue': 'white'}`
  `

  return <Div primary></Div>
}

Router 5

Router 毋容置疑是框架中不可或缺的一部分。虽然 react-router-dom 已经升级到了 6,毕竟是没多久之前的事情。现在 5 仍然是主流。安装 npm install react-router-dom@5

路由使用

  • Route 示例中 path 为路径(注意 path 默认是模糊匹配模式),component 为组件;也可以直接使用 render 后跟渲染函数。

  • HashRouterhash 模式下的 Route,对应也有 BrowserRouter 对应 HTML5 下的 Route

import { HashRouter, Route } from 'react-router-dom';

import Home from './views/Home';
import About from './views/About';

export default function App() {
  return (
    {/* Vue 中的 hashRouter */}
    <HashRouter>
      {/* 类似于 Vue 中的 router-view,但是使用起来总感觉有些麻烦。 */}
      <Route path="/home" component={Home}></Route>
      <Route path="/about" component={About}></Route>
    </HashRouter>
  );
}

重定向路由 Redirect

from 来自哪个路径,也是模糊匹配。to 重定向到哪里。可以使对象或者字符串。

import React from 'react';
import { HashRouter, Route, Redirect } from 'react-router-dom';

export default function App() {
  return (
    <div>
      <HashRouter>
        <Route path="/home" component={Home}></Route>
        <Route path="/about" component={About}></Route>
        {/*
          bug: 此时在页面输入 /home/xxx 
          1,仍然显示 Home 组件。
          2,刷新页面后路由显示为 /home,实际应该为 /home/xxx 然后显示 404页面。
        */}
        <Redirect from="/" to="home" />
      </HashRouter>
    </div>
  );
}

导致以上问题的原因是,path 为模糊匹配,/home/xxx 当匹配到 /home 的时候就已经不会再往下走了。为了解决问题 ① ② 可以使用 Switch 和添加 404 组件解决。

Switch 保证只会渲染一个 Route

import { HashRouter, Route, Redirect, Switch } from 'react-router-dom';
import NotFound from './views/NotFound';

export default function App() {
  return (
    <div>
      <HashRouter>
        <Switch>
          <Route path="/home" component={Home} exact></Route>
          <Route path="/about" component={About} exact></Route>
          <Redirect from="/" to="home" exact />
          {/** 给上面的组件全部添加上 exact 避免使用模糊匹配模式,此时如果匹配不上就会到最后一个,显示 404 组件。 */}
          <Route path="*" component={NotFound} />
        </Switch>
      </HashRouter>
    </div>
  );
}

嵌套路由

根据以上案例,如果 /home 下存在两个子集路由,只需要按照以上类似的配置。在 Home.jsx 中重新配置即可。

需要注意的是当存在子路由时需要去除父路由的 exact 选项,以便路由的匹配规则顺利进入子路由。

import { Route, Switch, Redirect } from 'react-router-dom';

import Activity from './home/Activity';
import Market from './home/Matket';

export default function Home() {
  return (
    <>
      <div className="banner" style={{ width: '100%', height: '300px', backgroundColor: 'hotpink' }}></div>
      <Switch>
        <Route path="/home/activity" component={Activity} exact />
        <Route path="/home/market" component={Market} exact />
        {/* 重定向 activity */}
        <Redirect from="/home" to="/home/activity" exact />
      </Switch>
    </>
  );
}

编程式导航

在以上的示例中,只是将相应的组件渲染到对应的地址中。相等于 RouterView,但此时还差一点内容 RouterLink router.push...

需求:基于以上的示例,假设我们需要一个 tab 栏,点击某一个 tab 就跳转到对应的页面。

基于模板的声明式导航

NavLink Link 相当于 RouterLink 就不在举例其他参数,文档查阅

import { NavLink, Link } from 'react-router-dom';

export default function Home(props) {
  return (
    <div>
      <div className="banner" style={{ width: '100%', height: '300px', backgroundColor: 'hotpink' }}></div>
      <NavLink to="/home/activity">activity</NavLink>
      <NavLink to={{ pathname: '/home/market' }}>market</NavLink><Switch>
        <Route path="/home/activity" component={Activity} exact />
        <Route path="/home/market" component={Market} exact />
        {/* 重定向 activity */}
        <Redirect from="/home" to="/home/activity" exact />
      </Switch>
    </div>
  );
}

基于 props 的编程式导航

需要注意的是如果父路由使用 render 的方式创建并没有手动把 props 传递 props 则获取不到内容

function Home() {
  return (
    <Switch>
      {/* 如果想要获取 props 需要手动传递。 */}
      <Route path="/home/activity" render={props => <Activity {...props} />} exact />
      <Route path="/home/market" render={() => <Market />} />
    </Switch>
  );
}
export default function Home(props) {
  return (
    <div>
      <div className="banner" style={{ width: '100%', height: '300px', backgroundColor: 'hotpink' }}></div>
      <div className="tab">
        {/* 第二个参数会放到 props.hisitory.location.state 中 */}
        <div onClick={() => props.history.push('/home/activity', { userInfo: { username: 'zhangsan' } })}>activity</div>
        <div onClick={() => props.history.replace('/home/market')}>market</div>
      </div>
      <Switch>
        <Route path="/home/activity" component={Activity} exact />
        <Route path="/home/market" component={Market} exact />
        {/* 重定向 activity */}
        <Redirect from="/home" to="/home/activity" exact />
      </Switch>
    </div>
  );
}

history 中包含了一些重要方法或者属性,简单列举。

  • goVueRouter 中的 go 方法完全一致。

  • goBack 回退到上一次的路由导航地址

  • goForwardgoBack 相反。

  • push replace 功能参考 VueRouter 同名 API,接收两个参数 path state 其中 state 会放到 props.history.location.state 中。

  • location 返回当前路由信息。

基于 useHistory 的编程式导航

import { useHistory } from 'react-router-dom';

export default function Home(props) {
  //  reactrouter 提供了 useHistory hook 功能和 props.history 一致。
  const history = useHistory();
  return (
    <div>
      <div className="banner" style={{ width: '100%', height: '300px', backgroundColor: 'hotpink' }}></div>
      <div className="tab">
        {/* 第二个参数会放到 hisitory.location.state 中 */}
        <div onClick={() => history.push('/home/activity', { userInfo: { username: 'zhangsan' } })}>activity</div>
        <div onClick={() => history.replace('/home/market')}>market</div>
      </div>
      <Switch>
        <Route path="/home/activity" component={Activity} exact />
        <Route path="/home/market" component={Market} exact />
        {/* 重定向 activity */}
        <Redirect from="/home" to="/home/activity" exact />
      </Switch>
    </div>
  );
}

动态路由

import React, { useState } from 'react';
import { Route, useHistory } from 'react-router-dom';

import Detail from './Detail';

export default function Matket() {
  const history = useHistory();

  const [list, setList] = useState([1, 2, 3, 4, 5, 6, 7, 8, 9]);

  const clickItem = item => {
    // 1,params 可以使用模板字符串传递。
    history.push({ pathname: `/home/market/detail/${item}` });
    // 2,动态参数 search 同样使用麻烦 和 vue中的 query 传参一致。但是叫法不一样。
    history.push({ pathname: `/home/market/detail?id=${item}` });
    // 3,query 参数传递
    history.push({ pathname: '/home/market/detail', query: { id: item } });
  };

  return (
    <div>
      <ul>
        {list.map(item => (
          <li
            key={item}
            style={{ backgroundColor: item % 2 === 0 ? 'hotpink' : 'skyblue', lineHeight: '30px', cursor: 'pointer' }}
            onClick={() => clickItem(item)}
          >
            {item}
          </li>
        ))}
      </ul>
      {/** 1,需要调整为动态地址,于 Vue 类似 */}
      <Route path="/home/market/detail/:id" component={Detail} />
      {/** 2,3, 4, 路由视图不在需要参数 */}
      <Route path="/home/market/detail" component={Detail} />
    </div>
  );
}

// Detail.jsx
function Detail(props) {
  // 1, 动态参数 params
  console.log(props.match.params.id);
  // 2, 动态参数 search 返回的是个字符串,不好取值。
  console.log(props.location.search);
  // 3, query 参数 刷新页面数据会丢失
  console.log(props.location.query);
  // 4, state 参数 刷新页面数据会丢失
  console.log(props.location.state);

  return <div>Detail</div>;
}

withRouter

根据 动态路由 的示例,已经来到了 detail 页面。如果 detail 页面中在封装一些组件并在组件中访问 history 的方法以及对象发现并不可行。

withRouter 方案 通过高阶组件,让被包裹的组件可以获取 route 的信息

import React from 'react';
import { withRouter } from 'react-router-dom';

export default function Detail(props) {
  return <WithDetailUser />;
}

function DetailUser(props) {
  return (
    <div>
      {Object.keys(props).length && (
        <>
          <div>userId: {props.match.params.id}</div>
          <div>userName: {props.location.query.username}</div>
          <div>userAge: {props.location.state.age}</div>
        </>
      )}
    </div>
  );
}

const WithDetailUser = withRouter(DetailUser);

hooks 方案 通过 ReactRouter 提供的一些 hook 解决此问题

import React from 'react';
import { useLocation, useRouteMatch } from 'react-router-dom';

export default function Detail(props) {
  return <DetailUser />
}

function DetailUser(props) {
  const location = useLocation();
  const match = useRouteMatch();
  return (
    <div>
      <div>userId: {match.params.id}</div>
      <div>userName: {location.query.username}</div>
      <div>userAge: {location.state.age}</div>
    </div>
  );
}

Router 6

react-router 的最新版本,比较上个版本体积更小,更加好用。但是会有些许区别。

起步

// 路由模式仍然没有更改
// 之前的 Switch 变更为了 Routes 并且必须在 Route 的外层使用。不然会有错误
// Route 相比较之前 render component 变更为了 element
import { BrowserRouter, Routes, Route } from 'react-router-dom';

import Film from './pages/Film';
import Center from './pages/Center';
import Cinema from './pages/Cinema';

function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path="/films" element={<Film />} />
          <Route path="/center" element={<Center />} />
          <Route path="/cinemas" element={<Cinema />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}
export default App;

重定向 Navigate

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';

function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          {/* 方案一 直接使用 Navigate 组件 */}
          {/* <Route path="/" element={<Navigate to="/films" replace />} /> */}

          {/* 方案二 使用 useNavigate 方法自定义 Redirect 组件 */}
          <Route path="/" element={<Redirect />} />

          <Route path="/films" element={<Film />} />
          <Route path="/center" element={<Center />} />
          <Route path="/cinemas" element={<Cinema />} />
          {/* 404 路由 */}
          <Route path="*" element={<NotFound />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}
const NotFound = () => <div>404</div>;
function Redirect() {
  const navigate = useNavigate();
  useEffect(() => {
    navigate('/films');
  }, [navigate]);
}
export default App;

嵌套路由

设计真的很赞,应该是借鉴了 vue 的特点,使用上很方便。嵌套路由可以使用 “插槽” 的方式使用。但是需要在嵌套理由的父路由中使用 Outlet 组件,类似于 router-view

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';

import Film from './pages/Film';
import ComingSoon from './pages/Film/ComingSoon';
import NowPlaying from './pages/Film/NowPlaying';
import Center from './pages/Center';
import Cinema from './pages/Cinema';
import NotFound from './pages/NotFound';

function App() {
  return (
    <>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Navigate to="/films" replace />} />
          <Route path="/films" element={<Film />}>
            {/* index 表示当加载到父路由时,navigate 重定向到 films/nowplaying */}
            <Route index element={<Navigate to="/films/nowplaying" replace />} />
            {/* 可以是相对路径也可以是绝对路径 */}
            <Route path="/films/nowplaying" element={<NowPlaying />} />
            <Route path="comingsoon" element={<ComingSoon />} />
          </Route>
          <Route path="/center" element={<Center />} />
          <Route path="/cinemas" element={<Cinema />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </BrowserRouter>
    </>
  );
}
export default App;

// Film.jsx
import { Outlet } from 'react-router-dom';
export default function Film() {
  return (
    <>
      Film <Outlet />
    </>
  )
}

声明式导航 Link NavLink

比如像一些列表跳转详情的需求,可以使用 Link 组件跳转。但是如果碰到导航栏,tab 栏等需要选中高亮样式时,使用 NavLink 则更加合适。

v6 版本去除了 activeClassNameactiveStyle 属性,接收 styleclassName 属性并且需要传递一个函数。这个函数的参数中会包含 isActive 是否选中的 flag 进行判断。如果不传递以上两个属性,则默认的类名为 active

import React from 'react';
import { NavLink, Outlet } from 'react-router-dom';

export default function Film() {
  return (
    <div>
      <NavLink
        to="/films/nowplaying"
        style={({ isActive }) => {
          return { color: isActive ? 'red' : '' };
        }}
      >
        正在热映
      </NavLink>
      <br />
      <NavLink to="/films/comingsoon" className={({ isActive }) => (isActive ? 'custom-active' : '')}>
        即将上映
      </NavLink>
      <Outlet />
    </div>
  );
}

编程式导航

编程式导航仍然使用 useNavigate 方法,但是会举例说明一些传参的情况。

querystring 传递参数

useSearchParams 表现为 url? 分割,刷新数据也不会丢失。就是外观有些丑

// 父级组件传递参数
import { Outlet, useNavigate } from 'react-router-dom';
import { Tabs } from 'antd-mobile';

export default function Film() {
  const navigate = useNavigate();

  const tabChange = path => {
    // ? 以后为 querystring 参数
    navigate(`${path}?limit=20&page=2`);
  };
  return (
    <div>
      <Tabs onChange={key => tabChange(key)}>
        <Tabs.Tab title="正在热映" key="/films/nowplaying">
          <Outlet />
        </Tabs.Tab>
        <Tabs.Tab title="即将上映" key="/films/comingsoon">
          <Outlet />
        </Tabs.Tab>
      </Tabs>
    </div>
  );
}
// 子集组件接收参数

import { useSearchParams } from 'react-router-dom';

export default function Film() {

  // searchParams 可以操作参数的工具函数对象
  // setSearchParams 修改参数的方法
  const [searchParams, setSearchParams] = useSearchParams();
  return (
    <div>
      limit: {searchParams.get('limit')} page: {searchParams.get('page')}
      <button onClick={() => setSearchParams({limit: 2, page:20})}>changeParams</button>
    </div>
  )
}

params 传递参数

useParams 参数存在于 url 中,也叫动态路由。

需求:基于以上示例,在 ”正在热映“ 中加个列表,点击列表进入详情页。

// 针对 Tab 进行改造
<Tabs.Tab title="正在热映" key="/films/nowplaying">
  <Outlet />
  {list.map(item => (
    <li key={item} onClick={() => navigate(`/films/${item}`)}>
      {item}
    </li>
  ))}
</Tabs.Tab>

// 针对路由表进行改造
<Routes>
  <Route path="/" element={<Navigate to="/films" replace />} />
  <Route path="/films" element={<Film />}>
    <Route index element={<Navigate to="/films/nowplaying" replace />} />
    <Route path="/films/nowplaying" element={<NowPlaying />} />
    <Route path="comingsoon" element={<ComingSoon />} />
  </Route>
  {/* 我们想让这个详情页和正在热映平级切换页面,所以定义成兄弟路由 */}
  <Route path="/films/:id" element={<Detail />} />
  <Route path="/center" element={<Center />} />
  <Route path="/cinemas" element={<Cinema />} />
  <Route path="*" element={<NotFound />} />
</Routes>

// 新增 Detail.jsx
import { useParams } from 'react-router-dom';

export default function Detail() {
  // 接收一个对象。就是传递过来参数的对象
  const params = useParams();
  return <div>Detail {params.id}</div>;
}

state 传递参数

useLocation url 不可见的数据,但是刷新数据仍然不会丢失。🌰 同样基于 params 案例在多传递一些信息。

// 针对 Tab 进行改造
<Tabs.Tab title="正在热映" key="/films/nowplaying">
  <Outlet />
  {list.map(item => (
    <li key={item} onClick={() => navigate(`/films/${item}`, { state: { username: 'zhangsan', age: 24 } })}>
      {item}
    </li>
  ))}
</Tabs.Tab>;

// Detail 获取信息
import React, { useEffect } from 'react';
import { useParams, useLocation } from 'react-router-dom';

export default function Detail() {
  // 于 v5 版本的 location 基本一致。缺少了路由的一些跳转方法。但是属性基本都有保留
  const location = useLocation();
  useEffect(() => {
    console.log(location.state);
  }, [location]);
}

Outlet 参数传递

useOutletContext 其实可以理解为给 router-view 传递属性。子组件就可以用 props 进行接收。

// 父路由传递数据
const [user, setUser] = useState({ username: 'zhangsan', age: 25 });
<Tabs onChange={path => navigate(path)}>
  <Tabs.Tab title="正在热映" key="/films/nowplaying">
    <Outlet context={[user, setUser]} />
  </Tabs.Tab>
  <Tabs.Tab title="即将上映" key="/films/comingsoon">
    <Outlet />
  </Tabs.Tab>
</Tabs>;

// 子路由获取数据
import { useEffect } from 'react';
import { useOutletContext } from 'react-router-dom';

export default function NowPlaying() {
  const context = useOutletContext();
  useEffect(() => {
    console.log(context);
  }, [context]);
  return <div>NowPlaying</div>;
}

获取路由信息

useMatch 接收一个路由路径字符串,返回匹配到该路径的信息。

import { useMatch } from 'react-router-dom';

const match = useMatch('/films/*');

useEffect(() => {
  // 返回路径包含 /films 的路径信息
  console.log(match.pathname);
}, [match]);

鉴权

reactRouter 的路由拦截相比较 vueRouter 还是简化了不少的。不再需要特定的钩子了。可以通过鉴权函数来判定是否跳转。

<Route
  path="/films/:id"
  element={
    <RequireAuth>
      <Detail />
    </RequireAuth>
  }
/>;

// 鉴权组件
function RequireAuth({ children }) {
  const token = 'xxx';
  if (!token) {
    return <Navigate to="/login" replace />;
  }

  return children;
}

懒加载

懒加载于 v5 版本基本一致。

const Cinema = React.lazy(() => import('./pages/Cinema'));

<Route
  path="/cinemas"
  element={
    <Suspense fallback={<>loading...</>}>
      <Cinema />
    </Suspense>
  }
/>;

路由表

useRoutes 可能是借鉴了 vueRouter 的使用习惯。相当的好用。通过写一个对象即可生成整个路由表。

需求: 改造之前的模板路由表

import { useRoutes } from 'react-router-dom';

function App() {
  const element = useRoutes([
    // 访问根路径的重定向
    { path: '', element: <Film /> },
    {
      path: '/films',
      element: <Film />,
      children: [
        // 重定向
        { path: '', element: <NowPlaying /> },
        { path: 'nowplaying', element: <NowPlaying /> },
        { path: 'comingsoon', element: <ComingSoon /> }
      ]
    },
    // 动态路由
    { path: '/films/:id', element: <Detail /> },
    {
      path: '/cinemas',
      element: (
        <Suspense fallback={<>loading...</>}>
          <Cinema />
        </Suspense>
      )
    },
    { path: '/center', element: <Center /> },
    { path: '*', element: <NotFound /> }
  ]);

  return element;
}

Redux

等价于 Vue 中的 Vuex 或者 Pinia,安装 npm install redux。与之不同的是 ReduxReact 并不是强耦合的。工作流程如下。

ReduxDataFlow.gif

redux 使用原则

  • state 单一对象存储在 store 中;

  • state 只读对象,每次都返回一个新的对象;

  • 使用纯函数 reducer 执行 state 更新。

起步

store

import { createStore } from 'redux';

// 初始化的 state
const state = {
  count: 0
};
// reducer 函数
const reducer = function (prevState = state, action) {
  const newState = { ...prevState };
  switch (action.type) {
    case 'increment':
      newState.count += 1;
      return newState;
    case 'decrease':
      newState.count -= 1;
      return newState;
    default:
      return prevState;
  }
};
// store 对象
const store = createStore(reducer);
export { store };

view

import React, { useEffect, useState } from 'react';
import { store } from './redux';

function Operation() {
  return (
    <>
      {/* dispatch 触发事件于 前面的 useReducer */}
      <button onClick={() => store.dispatch({ type: 'increment' })}>increment</button>
      <Counter />
      <button onClick={() => store.dispatch({ type: 'decrease' })}>decrease</button>
    </>
  );
}

function Counter() {
  // store.getState() 返回 store 的 state 信息
  const [count, setCount] = useState(store.getState().count);

  useEffect(() => {
    // subscribe 只有 dispatch 之后才会触发。
    store.subscribe(() => {
      setCount(store.getState().count);
    });
  }, []);

  return <div>{count}</div>;
}

Store 拆分

Vuex 中的 modules 类似。按照功能或者模块拆分出多个 store 方便代码组织以及团队协作。

import { createStore, combineReducers } from 'redux';
// 将定义的 count 的 reducer 拆分出去
// const state = {
//   count: 0
// };
// const counterStore = function (prevState = state, action) {
//   const newState = { ...prevState };
//   switch (action.type) {
//     case 'increment':
//       newState.count += 1;
//       return newState;
//     case 'decrease':
//       newState.count -= 1;
//       return newState;
//     default:
//       return prevState;
//   }
// };
// export default counterStore
import counterStore from './counterStore';
import globalSettingStore from './globalSettingStore';

// 通过 combineRuducer 进行合并。
const reducer = combineReducers({ counterStore, globalSettingStore });
const store = createStore(reducer);
export { store };

异步 Store 之 redux-thunk

都知道在 Vue 中异步的操作可以放到 actions 中去操作。但是 redux 就没有这么的开箱即用了。是用 redux 操作异步。推荐提前安装两个插件。

# 这两个插件任意一个都可以操作异步任务,只是风格不同。
npm install redux-thunk redux-promise

🌰 获取猫眼电影的 cinema 数据 记得提前配置接口代理 代理配置指南

1,定义数据 store

// cinemaStore.js
const state = {
  cinemas: []
};
const cinemaReducer = function (prevState = state, action) {
  const newState = { ...prevState };
  switch (action.type) {
    case 'getCinemas':
      newState.cinemas = action.payload;
      return newState;
    case 'setCinemas':
      newState.cinemas = action.payload;
      return newState;
    default:
      return prevState;
  }
};
export default cinemaReducer;

2,在 rootStore 中注册安装的中间件

import { combineReducers, createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reduxPromise from 'redux-promise';

const reducer = combineReducers({ cinemaReducer });

// 再次提醒,两种插件提供两种不同操作异步的方案。
const store = createStore(reducer, applyMiddleware(thunk, reduxPromise));

export { store };

3, 在 App 中实现如下需求。当 Store 中存在 cinema 数据的时候就直接从 store 中获取,不存在则接口请求。

import React, { useEffect, useState } from 'react';
import { store } from './redux';
import axios from 'axios';

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <div>
      {/** 当组件挂载获取卸载的时候,就会触发对应的缓存或者订阅 */}
      <button onClick={() => setShow(!show)}>toggle</button>
      {show && <Child />}
    </div>
  );
}

function Child() {
  useEffect(() => {
    if (!store.getState().cinemaReducer.cinemas.length) {
      // 1, redux-thunk 方式
      function getCinemas() {
        return dispatch => {
          axios
            .get(
              '/api/ajax/moreCinemas?movieId=0&day=2022-07-20&offset=0&limit=20&districtId=-1&lineId=-1&hallType=-1&brandId=-1&serviceId=-1&areaId=-1&stationId=-1&item=&updateShowDay=true&reqId=1658330425393&cityId=10&optimus_uuid=CB1F7A00035311ED861D91A1316D61D997062AB2573644BDAE97FD1BC27D80D4&optimus_risk_level=71&optimus_code=10'
            )
            .then(({ data }) => {
              dispatch({ type: 'getCinemas', payload: data.cinemas.cinemas });
            });
        };
      }

      // 2, redux-promise 方式
      // function getCinemas() {
      //   return axios
      //     .get(
      //       '/api/ajax/moreCinemas?movieId=0&day=2022-07-20&offset=0&limit=20&districtId=-1&lineId=-1&hallType=-1&brandId=-1&serviceId=-1&areaId=-1&stationId=-1&item=&updateShowDay=true&reqId=1658330425393&cityId=10&optimus_uuid=CB1F7A00035311ED861D91A1316D61D997062AB2573644BDAE97FD1BC27D80D4&optimus_risk_level=71&optimus_code=10'
      //     )
      //     .then(({ data }) => {
      //       return { type: 'getCinemas', payload: data.cinemas.cinemas };
      //     });
      // }
      store.dispatch(getCinemas());
    } else {
      // 获取缓存
      console.log(store.getState().cinemaReducer.cinemas, '缓存');
    }

    store.subscribe(() => {
      console.log(store.getState().cinemaReducer.cinemas, '订阅');
    });
  }, []);
}

异步 Store 之 redux-saga

redux-thunk 一样用来解决异步的问题。但是使用层面确有很大的不同。(这里不得不吐槽一下,react 的状态管理真的麻烦。不如 vue)在使用之前仍然需要先安装 npm install redux-saga

  • 步骤一:创建 reducer 函数
// redux/listReducer.js
const initialState = {
  list: []
};
const reducer = (state = initialState, action) => {
  const newState = { ...state };
  switch (action.type) {
    case 'GET_LIST':
      newState.list = action.payload;
      return newState;
    default:
      return state;
  }
};
export default reducer;
  • 步骤二:创建 store 于之前使用方式些许不同。
// redux/index
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from '@redux-saga/core';

// reducer
import reducer from './reducer';
// saga 函数
import watchSaga from './saga';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
// 侦听 saga 函数,当页面中触发 dispatch 时会首先进入 saga 逻辑中执行。
sagaMiddleware.run(watchSaga);
export { store };
  • 步骤三:创建 saga
// saga/index
import { take, fork, call, put } from 'redux-saga/effects';

// 需要注意的是 这是一个生成器函数。
export default function* watchSaga() {
  while (true) {
    // 页面触发的 dispatch 会被这里拦截
    yield take('GET_SAGA_LIST');
    // fork 触发异步逻辑
    yield fork(getList);
  }
}
function* getList() {
  // 执行异步代码
  const result = yield call(getFetchData);
  // 触发reducer的dispatch
  yield put({ type: 'GET_LIST', payload: result });
}

function getFetchData() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve([1, 2, 3, 4]);
    }, 2000);
  });
}
  • 步骤四:页面测试
import { useEffect } from 'react';
import { store } from './redux';

export default function App() {
  useEffect(() => {
    if (store.getState().list.length) {
      console.log('cache', store.getState().list);
    } else {
      // 触发 dispatch 会首先被 saga 拦截
      store.dispatch({ type: 'GET_SAGA_LIST' });
      console.log('store', store.getState().list);
    }
  }, []);
  return <div>App</div>;
}

多模块管理

需要用 saga 中的 all 方法

// saga/index
import { all } from 'redux-saga/effects';
import { listSaga } from './listSaga';
import { countSaga } from './countSaga';

export default function* watchSaga() {
  yield all([listSaga, countSaga]);
}
// 随后在 store中进行导入

react-redux

以上对于 redux 的操作实际上都是和 react 解耦的,无论在任意框架都可以使用。下面就开始演示与 React 结合的部分。在开始之前需要安装 npm install react-redux

还是以 count 为例,这次就不在需要 subscribe 也可以实时的获取到最新的值。

// store 的代码不变,只需要修改 jsx 相关代码即可。
import React from 'react';
import { Provider, connect } from 'react-redux';
import { store } from './redux';

export default function App() {
  return (
    // 使用 Provider 把 store 的数据注入到子组件中。
    <Provider store={store}>
      <div>
        <Child />
      </div>
    </Provider>
  );
}

let Child = props => {
  const { incrment, decrease, count } = props;
  return (
    <div>
      <button onClick={incrment}>increment</button>
      <div>{count}</div>
      <button onClick={decrease}>decrease</button>
    </div>
  );
};

// 获取 state 的信息并且映射。类似于 Vuex 中的 mapState
const mapStateToProps = state => ({ count: state.count });
// 将 dispatch 进行映射。
const mapDispatchToProps = dispatch => {
  return {
    incrment: () => dispatch({ type: 'increment' }),
    decrease: () => dispatch({ type: 'decrease' })
  };
};

// 使用 connect 高阶组件包裹 接收 state 和 dispatch 映射, 最终 state 和 dispatch 都会在被 connect 包裹的组件中的 props 中
Child = connect(mapStateToProps, mapDispatchToProps)(Child);

异步版本

import React, { useEffect, useState } from 'react';
import { connect, Provider } from 'react-redux';
import { store } from './redux';
import axios from 'axios';

export default function App() {
  const [show, setShow] = useState(true);
  return (
    <Provider store={store}>
      <div>
        <button onClick={() => setShow(!show)}>toggle</button>
        {show && <Child />}
      </div>
    </Provider>
  );
}

let Child = props => {
  const { cinemas, getCinemas } = props;
  useEffect(() => {
    if (cinemas.length) {
      console.log(cinemas, 'store 获取');
    } else {
      getCinemas();
      console.log('api 获取');
    }
  }, [cinemas, getCinemas]);

  return <div>Child组件</div>;
};

const mapStateToProps = function (state) {
  return {
    cinemas: state.cinemaReducer.cinemas
  };
};
const mapDispatchToProps = function (dispatch) {
  return {
    // 异步的任务不在需要考虑返回什么样的格式,直接在异步任务内也可以使用 dispatch `react-redux` 会帮助处理
    getCinemas: () => {
      return axios
        .get(
          '/api/ajax/moreCinemas?movieId=0&day=2022-07-20&offset=0&limit=20&districtId=-1&lineId=-1&hallType=-1&brandId=-1&serviceId=-1&areaId=-1&stationId=-1&item=&updateShowDay=true&reqId=1658330425393&cityId=10&optimus_uuid=CB1F7A00035311ED861D91A1316D61D997062AB2573644BDAE97FD1BC27D80D4&optimus_risk_level=71&optimus_code=10'
        )
        .then(({ data }) => {
          dispatch({ type: 'GET_CINEMAS', payload: data.cinemas.cinemas });
        });
    }
  };
};

// 如果不需要这两个参数可以传递 null
Child = connect(mapStateToProps, mapDispatchToProps)(Child);

持久化 Store

在正常的业务场景中,肯定有一些不想删除后就重置的数据,可以持久化的保存数据 redux-persist

TS

TS 应用的项目中,前提要有一定的 ts 基础。自荐 1.4w 字总结带你重学 TypeScript。总结下来就是日常业务其实在掌握 ts 之后并没有多大的差距。但是一定了解一些 React 内部的类型。这样才能得心应手

🌰 类组件中 state props ref 的类型定义

import React, { Component } from 'react';

interface State {
  count: number;
  username: string;
  age: number;
}
interface Props {
  username: string;
  age: number;
  changeName: () => void;
}

export default class App extends Component<Props, State> {
  state = {
    count: 0,
    username: 'zhangsan',
    age: 24
  };

  inputRef: React.Ref<HTMLInputElement> | null = React.createRef();

  render(): React.ReactNode {
    return <div></div>
  }
}

🌰 函数式组件中 state props ref 的类型定义

import React from 'react';
import { useRef } from 'react';
import { useState } from 'react';

interface User {
  username: string;
  age: number;
}
interface Props {
  username: string;
  age: number;
  changeName: () => void;
}

export default function App(props: Props) {
  const [count, setCount] = useState<number>(0);
  const [userInfo, setUserInfo] = useState<User>({ username: 'zhangsan', age: 24 });
  const inputRef = useRef<HTMLInputElement | null>(null);
}

Route

在使用 Route 的时候需要提前下载类型声明文件。npm install @types/react-router-dom -D

🌰 嵌套路由 动态路由 编程式导航的 ts 使用

import React, { Component } from 'react';
import { HashRouter, Switch, Route, NavLink, Redirect } from 'react-router-dom';
import type { RouteChildrenProps } from 'react-router-dom';

export default class App extends Component {
  render(): React.ReactNode {
    return (
      <>
        <HashRouter>
          <NavLink to="/home">Home</NavLink> | <NavLink to="/about">About</NavLink>
          <Switch>
            <Route path="/home" component={Home} />
            <Route path="/about" component={About} />
            <Redirect to="/home" />
          </Switch>
        </HashRouter>
      </>
    );
  }
}

class Home extends Component<RouteChildrenProps, { list: Array<number> }> {
  state = {
    list: [1, 2, 3, 4, 5, 6, 7, 8, 9]
  };
  render(): React.ReactNode {
    return (
      <>
        <div>Home</div>
        <ul>
          {this.state.list.map(item => (
            <li
              key={item}
              style={{ backgroundColor: item % 2 === 0 ? 'pink' : 'skyblue', lineHeight: '30px' }}
              onClick={() => this.props.history.push({ pathname: `/home/detail/${item}` })}
            >
              {item}
            </li>
          ))}
        </ul>

        <Route path="/home/detail/:id" component={Detail} />
      </>
    );
  }
}
class Detail extends Component<RouteChildrenProps<{ id: string }>, any> {
  render(): React.ReactNode {
    return (
      <>
        <div>Detail paramsId: {this.props.match?.params.id}</div>
      </>
    );
  }
}

class About extends Component {
  render(): React.ReactNode {
    return (
      <>
        <div>About</div>
        <Route path="/about/mine" component={Mine} />
        <Redirect to="/about/mine" />
      </>
    );
  }
}
class Mine extends Component<RouteChildrenProps, any> {
  render(): React.ReactNode {
    return <div>Mine pathName: {this.props.location.pathname}</div>;
  }
}

Redux

针对 reduxreducer 的书写这里就不再演示。其于基本函数,对象的 TS 基本没有差别。具体说明在 .tsx 中如何使用。

🌰 同步 redux

import React from 'react';
import { Provider, connect } from 'react-redux';
import { store } from '../redux';

import type { MapStateToProps, MapDispatchToPropsFunction } from 'react-redux';
import type { CountInitialState } from '../redux/reducers/countReducer';

export default function App() {
  return (
    <Provider store={store}>
      <ConnectChild />
    </Provider>
  );
}

type Props = TDispatchProps & CountInitialState;

let Child = (props: Props) => {
  return (
    <>
      <div>{props.count}</div>
      <button onClick={props.increment}>increment</button>
      <button onClick={props.decrease}>decrease</button>
    </>
  );
};

interface TDispatchProps {
  increment: () => void;
  decrease: () => void;
}

const mapStateToProps: MapStateToProps<CountInitialState, any, Record<'countReducer', CountInitialState>> = state => {
  return {
    count: state.countReducer.count
  };
};
const mapDispatchToProps: MapDispatchToPropsFunction<TDispatchProps, any> = dispatch => {
  return {
    increment: () => dispatch({ type: 'INCREMENT' }),
    decrease: () => dispatch({ type: 'DECREASE' })
  };
};

const ConnectChild = connect(mapStateToProps, mapDispatchToProps)(Child);

🌰 异步 redux

import React, { useEffect } from 'react';
import { Provider, connect } from 'react-redux';
import axios from 'axios';
import { store } from '../redux';

import type { MapStateToProps, MapDispatchToPropsFunction } from 'react-redux';
import type { CinemaState } from '../redux/reducers/cinemaReducer';

interface IDispatchProps {
  getCinemas: () => Promise<any>;
}

type Props = IDispatchProps & CinemaState;

export default function App() {
  return (
    <Provider store={store}>
      <SyncConnectChild />
    </Provider>
  );
}

function Child(props: Props) {
  const { getCinemas, cinemas } = props;

  useEffect(() => {
    getCinemas();
    console.log(cinemas);
  }, [getCinemas, cinemas]);
}

const mapStateToProps: MapStateToProps<CinemaState, any, Record<'cinemaReducer', CinemaState>> = state => {
  return {
    cinemas: state.cinemaReducer.cinemas
  };
};
// 返回异步时 注意 IDispatch 类型
const mapDispatchToProps: MapDispatchToPropsFunction<IDispatchProps, any> = dispatch => {
  return {
    getCinemas: () => {
      return axios
        .get(
          '/api/ajax/moreCinemas?movieId=0&day=2022-07-20&offset=0&limit=20&districtId=-1&lineId=-1&hallType=-1&brandId=-1&serviceId=-1&areaId=-1&stationId=-1&item=&updateShowDay=true&reqId=1658330425393&cityId=10&optimus_uuid=CB1F7A00035311ED861D91A1316D61D997062AB2573644BDAE97FD1BC27D80D4&optimus_risk_level=71&optimus_code=10'
        )
        .then(({ data }) => {
          dispatch({ type: 'GET_CINEMAS', payload: data.cinemas.cinemas });
        });
    }
  };
};

const SyncConnectChild = connect(mapStateToProps, mapDispatchToProps)(Child);

Dva

Dva 读音 我不玩守望先锋哇 😭,经过各种文献最终确认应该是这样的 [ˈdiːvə]。

Dva 是一个基于 reduxredux-saga 的数据流方案。额外还内置了 react-routerfetch。可以说是轻量级的应用框架。但是现在不推荐直接使用,推荐结合 umi 后续考虑在整理一版 umi 笔记。

Dva 文档 ,在开始之前强烈建议根据文档中的快速上手章节完整的做一做。快速上手

目录结构

.
├── assets            静态资源
├── components        组件
├── index.css         入口样式
├── index.js          入口文件
├── models            store 文件
├── router.js         路由文件
├── routes            页面 类似于 vue的 page 或 views 文件夹
├── services          推荐在内部定义一些请求的方法
└── utils
    └── request.js    dva 内置基于 fetch 封装的 request 方法

Router

在页面中使用的方式基本没有改变。需要注意的是,路由的引入不在是 react-router-dom 而是 dva/router

比如 import { Link, Route, Switch } from 'dva/router';,如果是编程式导航可以直接使用 props 接收。因为在入口文件中 history已经被注入了。

function RouterConfig({ history }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/" exact component={Products} />
      </Switch>
    </Router>
  );
}

Store

store 的使用层面,页面文件中几乎没有区别,仍然和 react-redux 一致,需要从 dva 导入 connect 对组件进行包装。

import { connect } from 'dva';

const Comp = () => <div>Component</div>;

export default connect(null, null)(Comp);

store 的定义方面,则需要在规定的 models 目录中进行。基于前面的快速上手示例进行修改。

  • 原有逻辑为。在入口文件中定义 initialState,在 Products.jsx 中获取数据并在 models 增加删除方法。

  • 现有逻辑一:迁移 initialStatemodels/products.js 中管理。通过增加一个 getList 的异步操作。

// services/products.js
// 模拟异步请求
export function getListService() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({
        products: [
          { name: 'dva', id: 1 },
          { name: 'antd', id: 2 }
        ]
      });
    }, 2000);
  });
}
// models/products.js
import { getListService } from '../services/products';

export default {
  // 于 vuex 的命名空间类似,在使用时也需要加上命名空间
  namespace: 'products',
  // 这就是 vuex 中的 state
  state: {
    products: []
  },
  // 对标 vuex 中的 mutations
  reducers: {
    /**
     * state  上面定义的 state
     * action 是 dispatch 传递过来的 action
     */
    getProducts(state, action) {
      return { ...state, products: action.payload };
    },
    deleteProducts(state, { payload: id }) {
      const products = state.products.filter(item => item.id !== id);
      return { ...state, products };
    }
  },
  // 对标 vuex 中的 actions
  effects: {
    /**
     * action dispatch 传递过来的 action
     * saga 可以解构出 saga 的相关方法
     */
    *syncGetProducts(action, { call, put }) {
      const result = yield call(getProductsService);
      // 内部调用可以不使用 模块名
      yield put({ type: 'getProducts', payload: result.products });
    }
  }
};
  • 随后在页面中进行操作。需要注意的是 dispatch 需要加上命名空间。
import React from 'react';
import { connect } from 'dva';
import ProductList from '../../components/ProductsList';

function Products({ dispatch, products }) {
  // 使用 dispatch 调用 delete 逻辑
  function handleDelete(id) {
    dispatch({
      type: 'products/deleteProducts',
      payload: id
    });
  }
  return (
    <div>
      <h2>
        List of Products
        {/* 使用dispatch 调用 get 异步逻辑 */}
        <button onClick={() => dispatch({ type: 'products/syncGetProducts' })}>getList</button>
      </h2>
      <ProductList onDelete={handleDelete} products={products} />
    </div>
  );
}
export default connect(state => ({
  // 访问时同样需要带上模块名
  products: state.products.products
}))(Products);

补充章节:项目实战

基于 react 的项目实践作业。感谢 千锋 kerwin 老师 带我重新温习了 react 相关全家桶的知识。这里也算是交上了一份作业。

实践视频破站地址,如果涉及到侵权望及时提醒改正。

项目仓库地址

页面基本为 1:1 复刻。但是在实现细节上可能有些许不同(毕竟 Vue 出身有一定的基础,主要注重一些接口的对接,页面实现两个框架其实没啥大的差异,思想基本相同),也可以给后续发现此内容的同学一个参考。

该项目技术栈使用:react18 react-router-dom@5 redux antd 为基本架构,后续加入了一些工具库 echarts lodash 等等。

后续如果有机会的话会考虑下 使用 umi react-router-dom@6 等其他技术构建其他的版本,当然这也是后话了。如果你查看这个项目没有其他的分支,那就是我胡咧咧。

项目启动

本项目在开始时一直使用 node v14.x 避免意外,请使用 14 及以上版本。

  • git clone https://github.com/ShuQingX/news-system-react.git

  • npm install

  • npm run serve 启动 json-server 的后端服务

  • npm start 启动前端

当然也有一些设计欠缺的地方。比如在 /api 这里,简单的认为项目没有这么大,就把接口全部放到一个文件,跟着视频越写发现文件越大,又不得不分开 😂。还有一些校验逻辑想当然的以为自己有些基础就提前设计好了。最后发现对接口字段的不理解,导致了最后路由鉴权的时候好多都是 hack 方案自己都看不下去了。不过好在功能还是实现了。(当然啦,我认为这都不是重点,重点是你能学到哪些内容)

项目演示就没有必要了,就象征性的放一个首页吧。

home.jpg