使用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方式,简单方便,相当于定义一个函数,然后再外面用到的地方调用,可以传参,可以控制调用的时机:
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
//父组件 <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;
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
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
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