使用react搭建组件库(二):react+typescript

1 使用了react官方脚手架:create-react-app

https://github.com/facebook/create-react-app

npm run eject 可以打开配置文件

自定义配置文件

执行安装: npx create-react-app ts-with-react --typescript 

npx 只有在npm5.2以上版本才有

1、避免安装全局模块:临时命令,使用后删除,再次执行的时候再次下载

2、调用项目内部安装的模块用起来更方便:比如 在package.json文件中安装了一个依赖:mocha,如果想执行有两种方法:

 2.1 在scripts中定义   

{
   "script":{
       "test":"mocha --version"
    }  
}

2.2 在命令行中执行  node_modules/.bin/mocha --version 

而使用npx的话 则只需要执行:  npx mocha --version 

首先新建一个hello组件:components/hello.jsx

import React from 'react'
interface IHelloProps {
  message?: string;//因为设置了props的默认值,这里用?表示有无都可
}

/*
const Hello = (props:IHelloProps) => {
  return <h2>{props.message}</h2>
}
*/
const Hello: React.FC<IHelloProps> = (props) => {
  //FC是 react 官方提供的一个 interface 的简写:FunctionComponent,接收一个范型
  //这样Hello包含了很多静态方法
  return <h2>{props.message}</h2>
}
Hello.defaultProps = {
  message: "Hello World"
}

export default Hello

 

## 什么是react hook

React 16.8 带来的全新特性,使用函数式组件,即将替代class组件的写法;

解决的问题

1.组件很难复用状态逻辑,一般使用HOC或者render Props

2.复杂组件难以理解,尤其是生命周期函数

例如,获取props中的id,需要在 componentDidMount和 componentDidUpdate 中同时定义;监听函数也需要在 componentDidMounted和componentWillUnmount中监听和注销

3.react组件一直是函数,使用Hook完全拥抱函数

 

## State Hook

 新建一个likeBotton.tsx 文件【点赞按钮】

import React, { useState } from 'react'
const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0);
  const [on,setOn] = useState(true);
  return (
    <>
    <button onClick={() => {setLike(like + 1)}}>
      {like} 👍
    </button>
    <button onClick={() => {setOn(!on)}}>
      {on?'ON':'OFF'}
    </button>
    </>
  )
}
export default LikeButton

网络请求、DOM操作 和函数渲染页面关系不大的 放在副作用中

Effect Hook

1.无需清除的Effect

2.需要清除的Effect

如果是以前的话,需要在两个生命周期中执行同样的代码

componentDidMount(){
  document.title = `you clicked ${this.state.count} times`;
}
componentDidUpdate(){
  document.title = `you clicked ${this.state.count} times`;
}

使用useEffect:

import React, { useState,useEffect } from 'react'
const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0);
  const [on,setOn] = useState(true);
  useEffect(()=>{//useEffect会在组件每次渲染的时候,都会被调用
    document.title = `点击了${like}次`
  })
  return (
    <>
    <button onClick={() => {setLike(like + 1)}}>
      {like} 👍
    </button>
    <button onClick={() => {setOn(!on)}}>
      {on?'ON':'OFF'}
    </button>
    </>
  )
}
export default LikeButton

2.需要清除的Effect

功能: 使用useEffect 完成一个鼠标跟踪器

原来在class中的实现方法:

componentDidMount(){
  document.addEventListener('click',this.updateMouse);
}
componentWillUnMount(){
  document.addEventListener('click',this.updateMouse);
}

使用useEffect,就要忘记以前的生命周期,页面挂载之后执行useEffect

import React, { useState, useEffect } from 'react'
const MouseTracker: React.FC = () => {
  const [ positions, setPositions ] = useState({x: 0, y: 0})
  useEffect(() => {
    console.log('add effect', positions.x)//执行顺序2:add effect 0;//执行顺序6:add effect 102
    const updateMouse= (e: MouseEvent) => {
      console.log('inner') //执行顺序3
      setPositions({ x: e.clientX, y: e.clientY })
    }
    document.addEventListener('click', updateMouse)
    return () => { //如果不加return函数中卸载功能,每次渲染函数组件的时候,都会执行useEffect
    //导致注册很多个 监听事件,
      console.log('remove effect', positions.x) //执行顺序5:remove effect 0
      document.removeEventListener('click', updateMouse)
    }
  }, [])
  //执行顺序1:before render 0//点击页面后,执行顺序4:before render 102
  console.log('before render', positions.x) 
  return (
    <p>X: {positions.x}, Y : {positions.y}</p>
  )
}
export default MouseTracker

 

但是每次更新函数组件,都要执行useEffect ,下面介绍如何控制useEffect函数的执行

useEffect(() => {
  console.log('document title effect is running')
  document.title = `点击了${like}次`;
  document.addEventListener('click', updateMouse)
  return ()=>{
    document.removeEventListener('click', updateMouse)
  }
}, [])
//useEffect第二个参数,如果是空数组,表示只在挂载元素后执行1次,但是其返回函数,会在组件卸载的时候执行
//如果不写第二个参数,则不做限制,组件中的任何一个data变化的时候,都会执行useEffect函数
//如果第二个参数写上变量后,则只有该变量发生变化的时候,才执行 useEffect

## 自定义Hook——新建的hooks,必须以 use 开头

1 将组件逻辑提取到可重用的函数中

 例如两个组件中均用到了公共的逻辑,获取跟随鼠标移动的坐标:

新建hooks/useMousePosition.jsx

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

const useMousePosition = () => {
  const [ positions, setPositions ] = useState({x: 0, y: 0})
  useEffect(() => {
    console.log('add effect', positions.x)
    const updateMouse= (e: MouseEvent) => {
      setPositions({ x: e.clientX, y: e.clientY })
    }
    document.addEventListener('mousemove', updateMouse)
    return () => {
      console.log('remove effect', positions.x)
      document.removeEventListener('mousemove', updateMouse)
    }
  }, [])
  return positions
}

export default useMousePosition

在其他组件中引入方式:

import useMousePosition from './hooks/useMousePosition'
const App:React.FC = () => {
  const positions  = useMousePosition();
  return (
    <div>
      <p>X:{position.x},Y:{positions.y}</p>
    </div>
  )
}

 

## 编辑一个hooks,功能是:加载图片功能,替换之前的HOC方式,简单方便,相当于定义一个函数,然后再外面用到的地方调用,可以传参,可以控制调用的时机:

react 中的 HOC 高阶组件,就是一个函数,接受一个组件作为参数,返回一个新的组件;
react可以通过高阶组件来扩展,而vue需要通过mixins来扩展。
 
 
interface IShowResult{
    message:string,
    status:string
}
//定义一个组件
const DogShow:React.FC<{data:IShowResult}> = ({data})=>{
    return (
        <>
            <h2>show:{data.status}</h2>
            <img src={data.message}/>
        </>
    )
}

const App:React.FC=()=>{
    //高阶组件,将一个组件用参数形式传入,然后经过包裹后返回一个新的组件,达到公用包裹组件的功能
    const WrappedDogShow = withLoader(DogShow,'https://dog.ceo/api/breeds/image/random');
    return (
        <WrappedDogShow/>
    )
}

高阶组件

 

// high order component
import React from 'react'
import axios from 'axios'

interface ILoaderState {
  data: any,
  isLoading: boolean
}
interface ILoaderProps {
  data: any,
}
const withLoader = <P extends ILoaderState>(WrappedComponent: React.ComponentType<P>, url: string) => {
  return class LoaderComponent extends React.Component<Partial<ILoaderProps>, ILoaderState> {
    constructor(props: any) {
      super(props)
      this.state = {
        data: null,
        isLoading: false
      }
    }
    componentDidMount() {
      this.setState({
        isLoading: true,
      })
      axios.get(url).then(result => {
        this.setState({
          data: result.data,
          isLoading: false
        })
      })
    }
    render() {
      const { data, isLoading } = this.state
      return (
        <>
          { (isLoading || !data) ? <p>data is loading</p> :
            <WrappedComponent {...this.props as P} data={data} />
          }
        </>
      )
    }
  }
}

export default withLoader
功能是传入一个组件,在高阶组件中,有三个功能
1、发送传入的url请求,得到需要的data值;
2、有一定的判断逻辑,没有拿到数据的时候显示 <p>,否则显示 传入的组件
3、给传入的组件添加data值
 
Vue中如何实现类似的功能呢?
 
//父组件
<template>
    <Loading><DogImg/></Lading>
</template>

//子组件
<template>
    <div>
        <p v-if="load">loading...</p>
        <div v-else><slot><img src="/location.png"/></slot></div>
    </div>
</template>
<script>
    {
        //需要传给父组件的data,使用 v-model 或者 vuex均可
    }
</script>

PS:react父子组件使用vue中的slot

import React, { Component } from 'react';
import Children from './Children';

class Father extends Component {

  render() {
      return (
        <div>
          <Children>
            <em>1111111111111111</em>
          </Children>
          <p>我是父组件</p>
        </div>
        
      )
  }
}

export default Father;

子组件

import React, { Component } from 'react';
class Children extends Component{
    constructor(props){
        super(props)
    }
    render(){
        return (
            <div>
                {this.props.children //这里调用了其内部} 
                <h1>我是子组件</h1>
            </div>
        )
    }
}
export default Children;
首先定义 hooks/useURLLoader.tsx
import { useState, useEffect } from 'react'
import axios from 'axios'
const useURLLoader = (url: string, deps: any[] = []) => {
  //第二个入参:deps,是决定该函数什么时候更新,因为放在了 useEffect 的第二个参数中
  //默认是空数组,也就是只执行一次
  const [data, setData] = useState<any>(null)
  const [loading, setLoading] = useState(false)
  useEffect(() => {
    setLoading(true)
    axios.get(url).then(result => {
      setData(result.data)
      setLoading(false)
    })
  }, deps)
  return [data, loading]
}
export default useURLLoader
然后在 App.jsx 中调用
 
import useURLLoader from './hooks/useURLLoader'
interface IShowResult {
  message: string;
  status: string;
}
const App: React.FC = () => {
  const [show,setShow] = useState(true)
  const [data, loading] = useURLLoader('http://www.xxx.com',[show]);//每次show变化的时候,都要更新hooks中的useEffect函数
  const dogResult = data as IShowResult;
  return (
    <div className="App">
    {
      loading?<p>图片下载中。。。</p>
      :<img src={dogResult && dogResult.message}/>
    }
    <p>
      <button onClick={() => {setShow(!show)}}>Refresh dog photo</button>
    </p>
    </div>
  );
}

export default App;

 

 useRef的使用

先来看 state和props的每次改变,其实都相当于闭包,每次的数据都是独立的:

比如下面代码,点击了第一个button后,三秒后才弹出 like 的值;在此期间多次点击第二个按钮,当前的 like 已经是其他的数字,但是异步执行的时候,保留的仍然是当时的数值;

import React, { useState, useEffect } from 'react'
const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0)
  useEffect(() => {
    console.log('document title effect is running')
    document.title = `点击了${like}次`
  }, [like])

  function handleAlertClick() {
    setTimeout(() => {
      alert('you clicked on ' + like)
    }, 3000)
  }
  return (
    <>
    <button onClick={() => {setLike(like + 1)}}>
      {like} 👍
    </button>
    <button onClick={handleAlertClick}> Alert!
    </button>
    </>
  )
}
export default LikeButton

所以使用useRef,注意的是修改ref的值并不会触发 render函数的更新,之所以函数更新是因为修改了 useState的值

import React, { useState, useEffect, useRef } from 'react'
const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0);//state在每一个render中都是独立的值,相当于闭包的存在
  const likeRef = useRef(0);//useRef 是一个函数,初始值是0;ref在所有的render中都保持的唯一的引用
  useEffect(() => {
    console.log('document title effect is running')
    document.title = `点击了${like}次`
  }, [like])

  function handleAlertClick() {
    setTimeout(() => {
      alert('you clicked on ' + likeRef.current)//改动了这里,以current获取值
    }, 3000)
  }
  return (
    <>
    <button onClick={() => {setLike(like + 1;likeRef.current++)}}>
      {like} 👍
    </button>
    <button onClick={handleAlertClick}> Alert!
    </button>
    </>
  )
}
export default LikeButton

 

前面useState只执行1次【模拟在生命周期:componentDidMount中执行】,是在第二个参数中,设置为[];
而如果useRef要求只执行1次【模拟在生命周期:componentDidMount中执行】,则设置标识位为false,改变后变为true,进行锁住;
这里实现的是,第一次不执行,待数据更新的时候再去执行
 
import React, { useState, useEffect, useRef } from 'react'
const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0);
  const didMountRef = useRef(false);
  useEffect(()=>{ //只有在数据更新的时候,才去执行
    if(didMountRef.current){
        console.log('is update');
    }else {
        didMountRef.current = true;
    }    
  })
  return (
    <>
    <button onClick={() => {setLike(like + 1)}}>
      {like} 👍
    </button>
    <button onClick={handleAlertClick}> Alert!
    </button>
    </>
  )
}
export default LikeButton

useRef常用来访问节点:

import React, { useState, useEffect, useRef } from 'react'
const LikeButton: React.FC = () => {
  const [like, setLike] = useState(0);
  const domRef = useRef<HTMLInputElement>(null);//虽然初始化的时候是null,但是其实是DOM元素的范性
  useEffect(()=>{ //每次渲染render的时候后会去执行,一旦获取到dom元素,则获取光标,而用 useRef 定义的DOM元素,是唯一的
      if(domRef && domRef.current){
          domRef.current.focus()
      }
  })
  return (
    <>
    <input type="text" ref = {domRef}/>
    <button onClick={() => {setLike(like + 1)}}>
      {like} 👍
    </button>
    <button onClick={handleAlertClick}> Alert!
    </button>
    </>
  )
}
export default LikeButton

 

使用useContext解决数据多层透传的问题

首先在最外层,父组件中使用:

 1 import React, { useState } from 'react';
 2 import LikeButton from './components/LikeButton'
 3 import Hello from './components/Hello'
 4 import useURLLoader from './hooks/useURLLoader'
 5 
 6 interface IThemeProps {
 7   [key: string]: {color: string; background: string;}
 8 }
 9 const themes: IThemeProps = {
10  'light': {
11    color: '#000',
12    background: '#eee',
13  },
14  'dark': {
15     color: '#fff',
16     background: '#222',
17   }
18 }
19 export const ThemeContext = React.createContext(themes.light) //定义context,且使用export导出
20 const App: React.FC = () => {
21   const [ show, setShow ] = useState(true)
22   //使用 ThemeContext.Provider 包裹所有的组件
23   return (
24     <div className="App">
25       <ThemeContext.Provider value={themes.dark}>
26       <header className="App-header">
27         <img src={logo} className="App-logo" alt="logo" />
28         <LikeButton />
29         <Hello />
30       </header>
31       </ThemeContext.Provider>
32     </div>
33   );
34 }
35 
36 export default App;

 

 然后在子组件中调用:
import React, { useState, useEffect, useRef, useContext } from 'react'//导入useContext
import { ThemeContext } from '../App' //自组件使用,首先要调用context
const LikeButton: React.FC = () => {
    const theme = useContext(ThemeContext)
    const style = {
        background: theme.background,
        color: theme.color,
    }
    return (
        <>
        <input type="text" ref={domRef} />
        <button style={style}>
            {like} 👍
        </button>
        </>
    )
}
export default LikeButton

 

Hook规则:
1.只在最顶层使用Hook,不要在循环和条件语句中使用hook
2.只在react函数中调用hook
 
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
 
posted @ 2020-04-05 11:58  小猪冒泡  阅读(1419)  评论(0编辑  收藏  举报