安装以ts为基础的react项目
create-react-app my-react-ts-app --template typescript
写一个hello world的小栗子
App.tsx 中添加Hello组件
import Hello from './components/Hello'
<Hello message='hello world' />
Hello.tsx的组件
import React from 'react'
interface IhelloProps {
message?: string
}
//FC 是FunctionComponent的类型别名
const Hello: React.FC<IhelloProps> = (props) => {
console.log(props.children)
return <h2>{props.message}</h2>
// return <h2>{props.children}</h2>
}
// React.FC 可以使用defaultProps
Hello.defaultProps = {
message: '222222222222'
}
export default Hello;
什么是react hook
react16.8版本带来的全新特性,即将替代class组件的写法
react有两种写法 一个是class一个是函数式(不能使用state和组件的生命周期的钩子函数, hook的推出就是为了打破这种壁垒)
react hook没有破坏性改动
完全可选
百分之百向后兼容
没有计划从react移除class
hook为了解决什么?
组件很难复用状态逻辑
复杂组件难以理解 尤其是生命周期函数
react组件一直是函数,使用hook完全是拥抱函数
自定义hook
将组件逻辑提取到可重用的函数中(高阶组件也可以)
案例 使用自定义hook抽象鼠标跟踪器
// 定义hook
import { useState, useEffect } from 'react';
// 自定义hook 必须用use开头,每次使用自定义hook的时候 里面的state都是隔离的
// useEffect 添加第二个参数 [] 表示 只会在初始化的执行一次 其他的时候只会更新数据
const useMousePosition = () => {
const [positions, setPositions] = useState({ x: 0, y: 0 })
useEffect(() => {
const updateMouse = (e: MouseEvent) => {
setPositions({ x: e.clientX, y: e.clientY })
}
document.addEventListener('mousemove', updateMouse)
return () => {
document.removeEventListener('mousemove', updateMouse)
}
}, [])
return positions
}
export default useMousePosition
使用hook -- 在需要的组件中
import React from 'react';
import logo from './logo.svg';
import './App.css';
import Hello from './components/Hello'
**使用hook -- 在需要的组件中**
import useMousePosition from './hooks/useMousePosition'
function App () {
// 调用自定义hook这个方法positions 或者这个方法返回的值
const positions = useMousePosition()
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
// 开始 根据hook函数返回的值 进行展示数据
<p>x: {positions.x}, y: {positions.y}</p>
// 结束
<Hello />
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
使用HOC的劣势 Higher order component
在异步请求时有个需求显示和隐藏loading的状态,发送请求的时候显示loading的状态,结束之后会隐藏loading,如果一直有这个需要,就需要把这个逻辑提取出来,在hook没有出现之前,使用HOC的使用方法--高阶组件
高级组件就是一个函数,接受一个组件作为参数,返回一个新的组件。
比如loading的实现和隐藏时一段逻辑 但是要分开写 并且添加了多了的节点
自定义hook的写法 完成loading的显示与隐藏 useSatet useEffect
把逻辑提取到一个函数中,通过调用函数来实现公共逻辑部分的实现
import axios from 'axios'
import { useState, useEffect } from 'react'
//any[] = [] 设置deps数组中可以时任意值 并且设置了默认值为空数组
const useURLLoading = (url: string, deps: any[] = []) => {
// data 设置默认null的时候会出错,使用泛型 添加<any>
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 useURLLoading;
使用:
import useURLLoading from './hooks/useURLLoading'
const positions = useMousePosition()
const [data, loading] = useURLLoading('https://dog.ceo/api/breeds/image/random')
const resultDate = data as IsShow;
...
{loading ? <p>读取中</p>
: <img alt='' src={resultDate && resultDate.message} />
}
遇到的问题:
Line 23:6: React Hook useEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependencies react-hooks/exhaustive-depseps
Line 23:6: React Hook useEffect has a missing dependency: 'url'. Either include it or remove the dependency array react-hooks/exhaustive-deps
问题: 自定义的hook给useEffect传入依赖时,eslint会自动产生警告,这是为什么呢
解决:当你的 effect 有任何外部依赖的时候,但是没有放入依赖数组的时候,就会出现这些警告,出现这些警告要结合实际情况,在这里我们可以警告提示我们需要把 fetchSuggestions 放入,添加这个依赖就可以了,在后面的内容中,我们是会处理这个依赖问题的
useRef 解决多次渲染中值是最新的问题
useRef的值改变不会渲染组件
const likeRef = useRef(0)
...
// 延迟3s打印出来的是最新的数据
function handleAlertClick () {
setTimeout(() => {
alert('you clicked on ' + likeRef.current)
}, 3000)
}
获取dom节点做一些操作
import React, { useState, useEffect, useRef } from 'react';
...
const domRef = useRef<HTMLInputElement>(null)
useEffect(() => {
if (domRef && domRef.current) {
domRef.current.focus()
} else {
console.log('1111111')
}
})
...
<input type="text" ref={domRef} />
useContext解决多层传递属性的林丹妙药
interface IThemeProps {
[key: string]: { color: string, background: string }
}
const themes: IThemeProps = {
'light': {
'color': '#000',
'background': '#eee'
},
'dark': {
'color': '#fff',
'background': '#222'
}
}
export const ThemeContext = React.createContext(themes.light)
// 将需要值得节点全部包括
// 开始
<ThemeContext.Provider value={themes.dark}>
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<input type="text" ref={domRef} />
<p> x: {positions.x}, y: {positions.y}</p>
{loading ? <p>读取中</p>
: <img alt='' src={resultDate && resultDate.message} />
}
<Hello />
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
// 结束
</ThemeContext.Provider>
// 在组件中使用,引用app中导出得值
import React, { useContext } from 'react'
import { ThemeContext } from '../App'
const Hello: React.FC<IhelloProps> = (props) => {
// 通过useContext使用导入得值
const theme = useContext(ThemeContext)
// 获取值之后 给需要得参数传递参数
const style = {
color: theme.color,
background: theme.background
}
// 使用
return <h2 style={style}>{props.message}</h2>
}
Hello.defaultProps = {
message: '111111111111'
}
export default Hello;
Hook规则
只在最顶层使用hook
只在react函数中使用hook
其他得hook
useRducer
useCallback
完成一个组件库需要考虑得问题
代码结构
样式解决方案
组件需求分析和编码
组件测试用例分析和编码
代码打包输出和发布
CI/CD,文档生成等等
从简单入手,在需求中慢慢复杂
import React, { FC } from 'react'
import classNames from 'classnames'
// export type ButtonSize = 'lg' | 'sm'
// export type ButtonType = 'primary' | 'default' | 'danger' | 'link'
// 或者使用enum 枚举
export enum ButtonSize {
Large = 'lg',
Small = 'sm'
}
export enum ButtonType {
Primary = 'primary',
Default = 'default',
Danger = 'danger',
Link = 'link',
}
interface BaseButtonProps {
className?: string,
disabled?: boolean,
size?: ButtonSize,
btnType?: ButtonType,
children: React.ReactNode,
href?: string
}
const Button: FC<BaseButtonProps> = (props) => {
const { className, disabled, size, btnType, children, href } = props
// btn, btn-lg, btn-primary
const classes = classNames('btn', {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
'disabled': (btnType === ButtonType.Link) && disabled
})
if (btnType === ButtonType.Link && href) {
return (
<a href={href} className={classes}>{children}</a>
)
} else {
return (
<button disabled={disabled} className={classes}>{children}</button>
)
}
}
Button.defaultProps = {
disabled: false,
btnType: ButtonType.Primary
}
export default Button;
/*
* @Author: your name
* @Date: 2021-05-14 16:09:22
* @LastEditTime: 2021-05-17 10:58:11
* @LastEditors: Please set LastEditors
* @Description: In User Settings Edit
* @FilePath: \my-react-ts-app\src\components\menu\menu.tsx
*/
import React, {useState,createContext} from 'react'
import classNames from 'classnames'
import {MentItemProps} from './menuItem'
export type MenuMode = 'horizontal' | 'vertical'
export type SelectCallBack = (selectIndex: number) => void
export interface MenuProps {
defaultIndex?: number;
className?: string;
mode?: MenuMode;
style?: React.CSSProperties;
onSelect?: SelectCallBack
}
interface IMenuContext {
index : number;
onSelect?: SelectCallBack
}
export const MenuContext = createContext<IMenuContext>({index: 0})
const Menu: React.FC<MenuProps> = (props) => {
const { defaultIndex, className, mode, style, onSelect, children } = props;
const [current, setIndex] = useState(defaultIndex)
const classnames = classNames('menu', className, {
'menu-vertical': mode === 'vertical'
})
const handleClick = (index:number) => {
setIndex(index)
if(onSelect){
onSelect(index)
}
}
const passedContext:IMenuContext ={
index: current ? current : 0,
onSelect: handleClick
}
//children是一个不透明得数据,react提供了用于处理this.props.children 不透明数据结构得实用方法
// 如果出现不符合规则得类型,map完美得跳过这些类型
// React.Children.map(children, function[(thisArg)])
const renderChildren = () => {
return React.Children.map(children, (child, index) => {
const childElement = child as React.FunctionComponentElement<MentItemProps>
const {displayName} = childElement.type
if(displayName === 'MenuItem'){
return React.cloneElement(childElement, {
index
})
}else{
console.error('warning22222222222222222');
}
})
}
return (
<ul className={classnames} style={style}>
<MenuContext.Provider value={passedContext}>
{renderChildren()}
</MenuContext.Provider>
</ul>
)
}
Menu.defaultProps = {
defaultIndex: 0,
mode: 'horizontal'
}
export default Menu
// 子组件menuItem
import React, {useContext} from 'react'
import classNames from 'classnames'
import {MenuContext} from './menu'
export interface MentItemProps {
index?: number;
disabled?: boolean;
className?: string;
style?: React.CSSProperties;
}
const MenuItem: React.FC<MentItemProps> = (props) => {
const {index, disabled, className, style, children} = props;
const context = useContext(MenuContext)
const classnames = classNames('menu-item', className, {
'is-disabled': disabled,
'is-actived': context.index === index
})
const handleClick = () => {
if(context.onSelect && !disabled && (typeof index ==='number')){
context.onSelect(index)
}
}
return (
<li className={classnames} style={style} onClick={handleClick}>
{children}
</li>
)
}
MenuItem.displayName = 'MenuItem'
export default MenuItem
// 调用
<Menu defaultIndex = {0} onSelect={(index) => {alert(index)}}>
<MenuItem>2222222222</MenuItem>
<MenuItem>2222222222</MenuItem>
<MenuItem>2222222222</MenuItem>
<MenuItem>2222222222</MenuItem>
</Menu>
//添加submenu 菜单 可点击二层菜单
import React, {useContext, useState} from 'react'
import classNames from 'classnames'
import {MenuContext} from './menu'
import {MentItemProps} from './menuItem'
export interface SubMenu{
index ?: number;
title: string;
className?: string
}
const SubMenu: React.FC<SubMenu> = ({index, title, className, children}) => {
const context = useContext(MenuContext)
const [menuOpen, setOpen] = useState(false)
const classnames = classNames('menu-item sub-menu', className, {
'is-active': context.index === index
})
const handleClick = (e:React.MouseEvent) => {
e.preventDefault()
setOpen(!menuOpen)
}
let timer: any
const handleMouse = (e:React.MouseEvent, toggle: boolean) => {
clearTimeout(timer)
e.preventDefault()
timer = setTimeout(() => {
setOpen(toggle)
}, 300);
}
const clickEvents = context.mode === 'vertical' ? {
onClick: handleClick
} : {}
const clickMouses = context.mode !== 'vertical' ? {
onMouseEnter: (e:React.MouseEvent) => {handleMouse(e, true)},
onMouseLeave: (e:React.MouseEvent) => {handleMouse(e, false)}
}: {}
const renderChildren = () => {
const subMenuClasses = classNames('viking-submenu', {
'menu-open': menuOpen
})
const childComponet = React.Children.map(children, (child, index) => {
const childElemet = child as React.FunctionComponentElement<MentItemProps>
if(childElemet.type.displayName === 'MenuItem'){
return childElemet
}else{
console.error('22222222222222');
}
})
return (
<ul className={subMenuClasses}>
{childComponet}
</ul>
)
}
return (
<li key={index} className={classnames} {...clickMouses}>
<div className="submenu-title" {...clickEvents}>
{title}
</div>
{renderChildren()}
</li>
)
}
SubMenu.displayName = 'SubMenu'
export default SubMenu
react中图标得二次封装
import React from 'react'
import classNames from "classnames";
import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome'
export type ThemeProps = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'danger'
export interface IconProps extends FontAwesomeIconProps {
theme?: ThemeProps
className?: string
}
const Icon: React.FC<IconProps> = (props) => {
const { className, theme, ...restProps } = props
const classes = classNames('viking-icon', className, {
[`icon-${theme}`]: theme
})
return (
<FontAwesomeIcon className={classes} {...restProps} />
)
}
export default Icon
// 使用
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
library.add(fas)
import Icon from './components/Icon/icon'
...
<Icon icon="acorn" theme="danger" size="10x"></Icon>
$theme-colors:
(
"primary": $primary,
"secondary": $secondary,
"success": $success,
"info": $info,
"warning": $warning,
"danger": $danger,
"light": $light,
"dark": $dark
);
@each $key,
$val in $theme-colors {
.icon-#{$key} {
color: $val;
}
}
<Icon icon="coffee" theme="primary" size="10x"></Icon>