React17基础

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2022-07-07

创建项目

npx create-react-app my-app --template typescript

tsx

1、采用类似HTML的语法,降低学习成本

2、充分利用JS自身的可编程能力创建HTML结构

3、并不是标准的JS语法,是JS的语法扩展,浏览器默认是不识别的,脚手架中内置的 @babel/plugin-transform-react-tsx包,用来解析该语法。我们写的 const aDiv = (<div className="a"><p>123</p></div>)编译之后类似与 const aDiv = React.createElement('div', { className: 'a' }, React.createElement('p', null, "123"))。其定义如下:

// 注意是 ...children
function createElement<P extends HTMLAttributes<T>, T extends HTMLElement>(
        type: keyof ReactHTML,
        props?: ClassAttributes<T> & P | null,
        ...children: ReactNode[]): DetailedReactHTMLElement<P, T>;

4、tsx必须有且只有一个根节点,可以使用 <></>来代替

组件状态

在react hook出来之前,函数式组件是没有自己的状态的,只有类组件有,react hook 解决了这个问题。

修改state与vue不一样,需要使用this.setState

不要直接修改状态的值,而是基于当前状态创建新的状态值

this.setState({
  count: this.state.count++,
  list: [
    ...this.state.list.slice(0, 2)
  ],
  obj: {
    ...this.state.obj,
    name: 'abc'
  }
})

事件绑定

// 类组件
class Test extends React.Component {
  constructor() {
    super();
    
    // 修正指向
    this.handleClick2 = this.handleClick2.bind(this);
  }

  handleClick = () => {
    console.log(this);
  }

  handleClick2() {
    console.log('2', this);
  }
  
  render() {
    return (
      <div>
        { /* 声明成箭头函数 直接this.xxx调用 */}
        <button onClick={this.handleClick}></button>
        <button onClick={() => this.handleClick('额外传参')}></button>
 
        { /* 声明成普通类方法 需要修正this执行 */}
        <button onClick={this.handleClick2}></button>

        { /* 声明成普通类方法 也可以想传参一样 用箭头函数再包一层 */}
        <button onClick={() => this.handleClick2()}></button>
      </div>
    )
  }
}

生命周期

只有类组件有生命周期,函数组件没有生命周期,因为类组件需要实例化,函数组件不需要实例化。

挂载阶段:

  • constructor:创建组件时,最先执行,初始化的时候只执行一次。初始化state,创建ref,使用bind解决this指向问题

  • render:每次组件渲染都会触发。不能再里面调用setState

  • componentDidMount:组件挂载后执行,初始化的时候只执行一次,发送网络请求,DOM操作

更新阶段:

  • render: 每次组件渲染都会触发。

  • componentDidUpdate:组件更新后,DOM渲染完毕,不要直接调用setState

卸载阶段:

  • componentWillUnmount:组件卸载,执行清理工作。

useState

1、只能出现在函数组件中

2、不能嵌套在if/for/其他函数中,react按照hooks的调用顺序识别每一个hook。

function Counter(props) {
  // useState传入的参数是该状态的初始值
  // 也可以传入一个回调函数,回调函数的return出去的值将作为name的初始值
  // 回调函数中的逻辑只会在组件初始化时执行一次
  const [count, setCount] = useState(() => {
    return props.count * 100;
  });

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

function App() {
  return (
    <div>
      <Counter count={10} />
      <Counter count={20} />
    </div>
  )
}

useEffect

什么是副作用?

副作用是相对于主作用来说,一个函数除了主作用来说,其他的作用都是副作用,对于React组件来说,主作用是根据数据渲染UI,除此之外都是副作用,比如手动修改DOM。

常见的副作用:数据请求ajax发送、手动修改DOM、localStorage操作

function UseEffectDemo() {

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

  useEffect(() => {
    document.title = "page" + count;
  }, []);
  // 第二个参数不传,副作用函数会在每次组件更新时都会执行一次
  // 第二个参数是一个依赖项,如果传入的是空数组,则只在首次渲染时执行一次
  // 添加特定的依赖项,副作用函数会在首次 以及 依赖项发生变化时重新执行

  // 在useEffect回调函数中用到的数据就是依赖数据,就应该出现在依赖项数组中,如果不添加依赖项就会有bug出现

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

清除副作用

import { useEffect, useState } from 'react';

function Test() {
  // 在加载时定时器就会启动
  // 当点击按钮时,组件隐藏/销毁了,定时器还在运行
  // 这个时候可以通过 useEffect中的回调函数中返回一个清除副作用的函数  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('定时器执行了')
    }, 1000);

    return () => {
      clearInterval(timer);
    }
  }, [])

  return (<div>this is test</div>)
}

function App() {
  const [flag, setFlag] = useState(true);

  return (
    <div>
      { flag ? <Test /> : null }
      <button onClick={() => setFlag(!flag)}> switch </button>
    </div>
  )
}

发送网络请求

// 因为只传一个空数组 与componentDidMount钩子类似,只在初始化的时候执行一次 且其回调函数是在dom渲染之后执行
useEffect(() => {

  async function loadData() {
    const res = await fetch('http://localhost:3000/data')
    console.log(res);
  }

  loadData();
}, [])

自定义hook

import { useState, useEffect } from 'react';

export function useWindowScroll() {
  const [y, setY] = useState(0);

  window.addEventListener('scroll', () => {
    setY(document.documentElement.scrollTop);
  })

  return [y];
}

export function useLocalStorage(key: string, defaultValue: any) {
  const [message, setMessage] = useState(defaultValue);

  // 当message发生变化时,重新将新值存储到本地
  useEffect(() => {
    window.localStorage.setItem(key, message);
  }, [message, key]);

  return [message, setMessage];
}

使用

function App() {
  const [y] = useWindowScroll();
  const [message, setMessage ] = useLocalStorage('testKey', "123");

  return (
    <div className="App">
      <p>UseWindowScroll: y: {y}</p>

      <br />
      <p>
      useLocalStorage: message: {message}

      <button onClick={() => setMessage("345")}>修改localStorage</button>
      </p>
    </div>
  );
}

export default App;

useRef

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

class TestC extends React.Component {
  state = {
    name: 'jack'
  }

  getName = () => {
    return this.state.name;
  }

  render(): React.ReactNode {
    return (<div> this is TestC {this.state.name } </div>)
  }
}

function MyUseRef() {
  const testRef = useRef(null) as unknown as React.MutableRefObject<TestC>;
  const pRef = useRef(null);

  useEffect(() => {
    console.log(testRef.current); // TestC { props: {} , state: { name: 'jack'}, getName: () => { return this.state.name; } .... }
    console.log(pRef.current); //   <p> this is p </p>

    const name = testRef.current?.getName();
    console.log(name); // jack
  }, [])

  return (
    <div>
      <TestC ref={testRef} />
      <p ref={pRef}> this is p </p>
    </div>
  )
}

export default MyUseRef;

useContext

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

const Context = createContext(0);

function ComB() {
  const count = useContext(Context);

  return <p>this is ComC app传过来的{ count }</p>
}

function ComA() {
  const count = useContext(Context);

  return (<div>
    <ComB />
    <p>this is ComA app传过来的{ count } </p>
  </div>)
}

function ContextDemo() {
  const [count, setCount] = useState(0);

  return (
    // 绑定在value上进行传递
    <Context.Provider value={count}>
       <div>
        <ComA />
        <button onClick={() => setCount(count+ 1)}>+</button>
      </div>
    </Context.Provider>
  )
}

export default ContextDemo;

Context如果要传递的数据 只需要在整个应用初始化的时候传递一次就可以选择在当前文件里做数据提供,如果需要传递数据并且将来还需要再对数据做修改,底层组件也需要跟着一起变的场景,可以包裹<App />来做数据提供。

React-Router

安装

yarn add react-router-dom@6

初步使用

import './App.css';
import Home from './view/Home';
import About  from './view/About';
import Prod  from './view/Prod';
import { BrowserRouter, Link, Routes, Route } from 'react-router-dom';

function App() {
  return (
    // 声明当前要用一个非hash模式的路由,还有一个HashRouter
    <BrowserRouter>
      {/* 指定跳转的组件 to用来配置路由地址  */}
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      <Link to="/prod">产品</Link>
      <Routes>
        <Route path="/" element={<Home />}></Route>
        <Route path="/about" element={<About />}></Route>
        <Route path="/prod/:id" element={<Prod />}></Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

路由导航

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

function About() {

  const navigate = useNavigate();

  function goHome() {
    // navigate('/')
    navigate('/', {
      replace: true,
      // state: {
      //   a: '123',
      // }
    })
  }

  function goProd1() {
    navigate('/prod?id=123');
    // 取参的时候 let [params] = useSearchParams(); let id = params.get('id');
  }

  function goProd2() {
    navigate('/prod/1001')
    // 要求是动态路由 否则会与/prod不匹配
    // <Route path="/prod/:id" element={<Prod />}></Route>
    // 取参的时候 let params = useParams(); let id = params.id;
  }

  return (<div>
    About

    <button onClick={() => goHome()}>跳转到首页</button>
    <button onClick={() => goProd1()}>跳转到产品页 searchParams</button>
    <button onClick={() => goProd2()}>跳转到产品页 Params</button>
  </div>)
}

export default About;

嵌套路由

import './App.css';
import Home from './view/Home';
import About  from './view/About';
import Prod  from './view/Prod';
import { BrowserRouter, Link, Routes, Route } from 'react-router-dom';
import Layout from './view/Layout';
import Board from './view/Board';
import Article from './view/Article';

function App() {
  return (
    <BrowserRouter>
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      <Link to="/prod">产品</Link>
      <Routes>
        <Route path="/" element={<Home />}></Route>
        <Route path="/about" element={<About />}></Route>
        <Route path="/prod/:id" element={<Prod />}></Route>
        <Route path="/layout" element={<Layout />}>
          {/* 嵌套路由,前面不需要加 / */}
          <Route path='board' element={<Board />}></Route>
          
          {/* 默认渲染的二级路由 去掉指定的path 加一个index */}
          <Route index element={<Article />}></Route>
          {/* <Route path='article' element={<Article />}></Route> */}
        </Route>
      </Routes>
    </BrowserRouter>
  );
}
export default App;

对应的Layout中渲染视图需要使用Outlet组件,与Vue的router-view类似。

// Layout.tsx
import { Outlet  } from 'react-router-dom';

export default function Layout() {
  return (<div>
    111222
    <Outlet />
  </div>)
}

404路由配置

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />}></Route>
        <Route path="/about" element={<About />}></Route>
        <Route path="/prod/:id" element={<Prod />}></Route>

        {/* 写在所有的路由的最下面,将path设置为* */}
        <Route path="*" element={(<div> 404 页面 </div>)}></Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

mobx

安装

mobx-react-lite 与 函数组件配合

mobx-react 与 类组价配合

yarn add mobx mobx-react-lite

使用

// .store/index.tsx
import { computed, makeAutoObservable } from 'mobx';

class CounterStore {
  // 定义数据
  count = 0;

  constructor() {
    // 设置为响应式
    makeAutoObservable(this, {
      // 定义计算属性
      countPlus: computed,
    });
  }

  // 定义action
  addCount = () => {
    this.count++
  }

  // 定义计算属性
  get countPlus() {
    return this.count * 10;
  }

}

// 实例化 然后导出
const counterStore = new CounterStore();
export { counterStore };
// App.js
// 1、引入store
import { counterStore } from './store';
// 2、引入中间件
import { observer } from 'mobx-react-lite';

function App() {
  return (
    <div className="app">
      { counterStore.count }
      <button onClick={() => counterStore.addCount()}>改变store</button>
    </div>
  )
}
// 包裹App
export default observer(App);

模块化

// 组合子模块
import { createContext, useContext } from 'react';
import CounterStore from './counter';
import ListStore from './list';


class RootStore {
  counterStore: CounterStore;
  listStore: ListStore;

  constructor() {
    this.counterStore = new CounterStore();
    this.listStore = new ListStore();
  }
}

const rootStore = new RootStore();
const context = createContext(rootStore);

// useContext优先从Provider value找,如果找不到 就会从createContext方法传递过来的默认参数
// 通过useContext方法拿到rootStore实例对象
const useStore = () => useContext(context);
// const useStore = () => useContext(createContext(new RootStore()))

export { useStore };

使用

import { useStore } from './store';
import { observer } from 'mobx-react-lite';

function App() {
  const { counterStore, listStore } = useStore();

  return (
    <div>
      <p> { counterStore.count }</p>
      <p> 计算属性: { counterStore.countPlus }</p>
      <button onClick={ counterStore.addCount}>改变store</button>
      {
        listStore.list.map(item => <p key={item}>{ item }</p>)
      }
    </div>
  );
}

export default observer(App);

路由鉴权

// 封装一个高阶组件
import { getToken } from '@/utils';
import { Navigate } from 'react-router-dom';

function AuthComponent({ children }) {
  const isToken = getToken();

  return isToken ? <>{ children }</> : <Navigate to="/login" replace />
}

export { AuthComponent }

使用

import { AuthComponent } from '@/components'

function App() {
  return (
    <BrowserRouter>
      <Link to="/">首页</Link>
      <Link to="/about">关于</Link>
      <Routes>
        <Route path="/" element={<Home />}></Route>
        <Route path="/about" element={
          <AuthComponent>
            <About />
          </AuthComponent>
        }></Route>
      </Routes>
    </BrowserRouter>
  )
}

CDN

const path = require('path');
const { whenProd, getPlugin, pluginByName } = require('@craco/craco');

module.exports = {
  // webpack配置
  webpack: {
    // 配置CDN
    configure: (webpackConfig) => {
      let cdn = {
        js: [],
        css: []
      }

      whenProd(() => {
        webpackConfig.externals = {
          react: 'React', // 对应的是 import React from 'react' 
          'react-dom': 'ReactDOM'
        }

        cdn = {
          js: [
            'https://cdn.bootcdn.net/ajax/libs/react/17.0.2/umd/react.production.min.js',
            'https://cdn.bootcdn.net/ajax/libs/react-dom/17.0.2/umd/react-don.production.min.js',
          ],
          css: []
        }
      })

      const { isFound, match } = getPlugin(webpackConfig, pluginByName('HtmlWebpackPlugin'));

      if (isFound) {
        // 找到了HTMLWebpackPlugin的插件
        match.userOptions.cdn = cdn;
      }

      return webpackConfig;
    }
  }
}

修改公共模板

<!-- index.html -->
<body>
  <% htmlWebpackPlugin.options.cdn.js.forEach(cdnUrl => { %>
    <script src="<%= cdnUrl %> "></script>
  <% }) %>
<body>

路由懒加载

import { lazy, Suspense } from 'react'; 

const login = lazy(() => import('./pages/Login'));

function App() {
  return (
    <HistoryRouter history={history}>
      <div className="App">
        <Suspense
          fallback={
            <div
              style={{
                textAlign: 'center',
                marginTop: 200
              }}
            >
              loading
            </div>
          }
        >
          <Routes>
            <Route path="/" element={<Home />}></Route>
            <Route path="/login" element={<Login />}></Route>
          </Routes>
        </Suspense>
      </div>
    </HistoryRouter>
  )
}