使用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 特性。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM