对函数型组件进行增强,让函数型组件可以存储状态,可以拥有处理副作用的能力,让开发者在不使用类组件的情况下,实现相同的功能。
为了复用逻辑增加无实际渲染效果的组件,增加了组件层级,显示十分臃肿,增加了调试的难度以及运行效率的降低
将一组相干的业务逻辑拆分到了多个生命周期函数中,在一个生命周期函数内,存在多个不相干的业务逻辑
Hooks 意为钩子, React Hooks 就是一堆钩子函数, React 通过这些钩子函数对函数型组件进行增强,不同的钩子函数提供了不同的功能
用于为函数组件引入状态
import {useState} from 'react'
function App() {
const [count, setCount] = useState(0)
return (
{count}
);
}
export default App;
const [count, setCount] = useState(0)
const [person, setPerson] = useState({
name: '张三', age: 20 })
const [count, setCount] = useState(0)
// 当初始值是动态值
// 这样写每次渲染都会执行
const propsCount = props.count || 0
// const [count, setCount] = useState(propsCount)
// 应该这样写
const [count, setCount] = useState(() => {
return props.count || 0 // 只有第一次执行的时候才会执行
})
function handleCount () {
setCount((count) => {
const newCount = count + 1
document.title = newCount
return newCount
})
}
useReducer 是另一种让函数组件保存状态的方式,可以将 dispatch 传给子组件使用
import { useReducer } from "react";
export default function App () {
function reducer (state, action) {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
return state;
}
}
const [count, dispatch] = useReducer(reducer, 0)
return (
{count}
)
}
在跨组件层级获取数据时简化获取数据的代码
import { createContext, useContext } from "react";
const countContext = createContext()
export default function App () {
return (
)
}
function Foo () {
const value = useContext(countContext)
return (
I am Foo {value}
)
}
让函数型组件拥有处理副作用的能力,类似生命周期函数
可以把 useEffect 看做 componentDidMount, componentDidUpdate 和 componentWillUnmount 这三个函数的组合
useEffect(() => {}) => componentDidMount, componentDidUpdate
useEffect(() => {}, []) => componentDidMount
useEffect(() => () => {}) => componentDidUpdate, componentWillUnmount
useEffect(() => () => {}, []) => componentWillUnmount
import { useEffect, useState } from "react";
import ReactDOM from 'react-dom'
export default function App () {
const [count, setCount] = useState(0)
// 组件挂载完成之后执行,组件数据更新之后执行
// useEffect(() => {
// console.log('123')
// })
// 组件挂载完成之后执行
// useEffect(() => {
// console.log('456')
// }, [])
useEffect(() => {
return () => {
console.log('组件被卸载了')
}
})
return (
{count}
)
}
(1). 为 window 对象添加滚动事件
(2). 设置定时器让 count 数值每隔一秒增加 1
import { useEffect, useState } from "react";
import ReactDOM from 'react-dom'
export default function App () {
const [count, setCount] = useState(0)
function onScroll () {
console.log('页面滚动了')
}
useEffect(() => {
window.addEventListener('scroll', onScroll)
return () => {
window.removeEventListener('scroll', onScroll)
}
}, [])
useEffect(() => {
const timerId = setInterval(() => {
setCount(count => {
const newCount = count + 1
document.title = newCount
return newCount
})
}, 1000);
return () => {
clearTimeout(timerId)
}
}, [])
return (
{count}
)
}
useEffect 解决的问题
(1). 按照用途将代码进行分类(将一组相同的业务逻辑归置到了同一个副作用函数中)
(2). 简化重复代码,是组件内部代码更加清晰
只有指定数据发生变化时触发 effect
import { useEffect, useState } from "react";
export default function App () {
const [count, setCount] = useState(0)
const [person, setPerson] = useState({name: '张三'})
useEffect(() => {
// person 的变化不会触发 useEffect , 因为第二个数组参数中只监听了 count
document.title = count
console.log(111)
}, [count])
return (
{count}
)
}
useEffect(() => {
(async () => {
await axios.get()
})()
}, [])
useMemo 的行为类似 Vue 中的计算属性,可以检测某个值的变化,根据变化只计算新值。
useMemo 会缓存计算结果,如果检测子没有发生变化,及时组建重新渲染,也不会重新计算,此行为可以有助于避免在每个渲染上进行昂贵的计算。
import { useState, useMemo } from "react";
export default function App () {
const [count, setCount] = useState(0)
const [bool, setBool] = useState(true)
const result = useMemo(() => {
console.log('111') // 只有 count 改变才会重新执行这个回调函数
return count * 2
}, [count])
return (
{count} {result}
{bool ? '真' : '假'}
)
}
性能优化,如果本组件中的数据没有发生变化,阻止组件更新,类似类组件中的 PureComponent 和 shouldComponentUpdate
import { memo } from 'react'
const Foo = memo(function Foo () {
return I am Foo
})
性能优化,缓存函数,使用组件重新渲染时得到相同的函数实例。否则每次父组件渲染,函数变量的实例都会变化,导致里层组件被重新渲染
import { useState, memo, useCallback } from "react";
const Foo = memo(function Foo (props) {
console.log('Foo 重新渲染了')
return
I am Foo
})
export default function App () {
const [count, setCount] = useState(0)
const resetCount = useCallback(() => {
setCount(0)
}, [setCount])
return (
{count}
)
}
import { useRef } from "react";
export default function App () {
const box = useRef()
return (
)
}
即使组件重新渲染,保存的数据仍然还在,保存的数据被更改不会触发组件重新渲染。
import { useRef, useState, useEffect} from "react";
export default function App () {
const [count, setCount] = useState(0)
let timeId = useRef() // 夸组件生命周期
useEffect(() => {
// 使用这个 ref 的 current 属性存储数据
timeId.current = setInterval(() => {
setCount(count => count + 1)
}, 1000);
}, [])
const stopCount = () => {
console.log(timeId.current)
clearInterval(timeId.current)
}
const box = useRef()
return (
{count}
)
}
自定义 Hook 是标准的封装和共享逻辑的方式
自定义 Hook 是一个函数,其名称以 use 开头
自定义 Hook 其实就是逻辑和内置 Hook 的组合
如何使用自定义 Hook:
import { useState, useEffect} from "react";
import axios from "axios";
function useGetPost () {
const [post, setPost] = useState({})
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => {
setPost(response.data)
})
}, [])
return [post, setPost]
}
export default function App () {
const [post, setPost] = useGetPost()
return (
{post.title}
{post.body}
)
}
封装公共逻辑:
import { useState} from "react";
function useUpdateInput (initialState) {
const [value, setValue] = useState(initialState)
return {
value,
onChange: e => setValue(e.target.value)
}
}
export default function App () {
const usernameInput = useUpdateInput('')
const passwordInput = useUpdateInput('')
const submitForm = event => {
event.preventDefault();
console.log(usernameInput.value)
console.log(passwordInput.value)
}
return (
)
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from "react-router-dom";
import App from './App';
ReactDOM.render(
,
document.getElementById('root')
);
App.js
import { Link, Route } from "react-router-dom";
import Home from './pages/Home'
import List from './pages/List'
export default function App () {
return (
<>
首页
列表页
>
)
}
Home.js
import { useHistory, useLocation, useRouteMatch, useParams } from "react-router-dom";
export default function Home(props) {
console.log(props)
console.log(useHistory())
console.log(useLocation())
console.log(useRouteMatch())
console.log(useParams())
return
Home works
;
}
输出结果:
{history: {…}, location: {…}, match: {…}, staticContext: undefined}
{length: 7, action: “PUSH”, location: {…}, createHref: ƒ, push: ƒ, …}
{pathname: “/home/zhangsan”, search: “”, hash: “”, state: undefined, key: “o6w5y3”}
{path: “/home/:name”, url: “/home/zhangsan”, isExact: true, params: {…}}
{name: “zhangsan”}
List.js
export default function List(props) {
console.log(props)
return
List works
;
}
使用数组 state 存储状态,用数组 setters 存储 setState方法,利用闭包管理,将 下标 stateIndex 缓存在闭包中,创建 对应的 setState.
// import { useState } from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function createSetter (index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setter = setters[stateIndex]
stateIndex ++
return [value, setter]
}
function render () {
stateIndex = 0
ReactDOM.render(
,
document.getElementById('root')
)
}
export default function App () {
const [count, setCount] = useState(0)
const [name, setName] = useState('张三')
return
{count}
{name}
}
// import { useState } from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function createSetter (index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setter = setters[stateIndex]
stateIndex ++
return [value, setter]
}
function render () {
stateIndex = 0
effectIndex = 0
ReactDOM.render(
,
document.getElementById('root')
)
}
// 上一次的依赖值
let prevDepsAry = []
let effectIndex = 0
/**
* useEffect
* @param {function} callback 回调函数
* @param {Array} depsAry 依赖数组
* @returns {function} 清理函数
*/
function useEffect (callback, depsAry) {
if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect 第一个参数必须是一个函数')
if (typeof depsAry === 'undefined') {
// 没有传递
callback()
} else {
// 判断 depsAry 是不是数组
if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect 第二个参数必须是一个数组')
// 获取上一次的状态值
let prevDeps = prevDepsAry[effectIndex]
// 将当前的依赖值和上一次的依赖值作对比,如果有变化,调用 callback
let hasChanged = prevDeps ? !depsAry.every((dep, index) => dep === prevDeps[index]) : true
// 判断值是否有变化
if (hasChanged) {
callback()
}
// 同步依赖值
prevDepsAry[effectIndex++] = depsAry
}
}
export default function App () {
const [count, setCount] = useState(0)
const [name, setName] = useState('张三')
useEffect(() => {
console.log('Hello')
}, [count])
useEffect(() => {
console.log('World')
}, [name])
// 测试不传监听数据的情况
useEffect(() => {
console.log('xxx')
}, [])
return
{count}
{name}
}
// import { useState } from 'react'
// import { useReducer } from 'react'
import ReactDOM from 'react-dom'
let state = []
let setters = []
let stateIndex = 0
function createSetter (index) {
return function (newState) {
state[index] = newState
render()
}
}
function useState (initialState) {
state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState
setters.push(createSetter(stateIndex))
let value = state[stateIndex]
let setter = setters[stateIndex]
stateIndex ++
return [value, setter]
}
function render () {
stateIndex = 0
effectIndex = 0
ReactDOM.render(
,
document.getElementById('root')
)
}
// 上一次的依赖值
let prevDepsAry = []
let effectIndex = 0
/**
* useEffect
* @param {function} callback 回调函数
* @param {Array} depsAry 依赖数组
* @returns {function} 清理函数
*/
function useEffect (callback, depsAry) {
if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect 第一个参数必须是一个函数')
if (typeof depsAry === 'undefined') {
// 没有传递
callback()
} else {
// 判断 depsAry 是不是数组
if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect 第二个参数必须是一个数组')
// 获取上一次的状态值
let prevDeps = prevDepsAry[effectIndex]
// 将当前的依赖值和上一次的依赖值作对比,如果有变化,调用 callback
let hasChanged = prevDeps ? !depsAry.every((dep, index) => dep === prevDeps[index]) : true
// 判断值是否有变化
if (hasChanged) {
callback()
}
// 同步依赖值
prevDepsAry[effectIndex++] = depsAry
}
}
function useReducer (reducer, initialState) {
const [state, setState] = useState(initialState)
function dispatch (action) {
const newState = reducer(state, action)
setState(newState)
}
return [state, dispatch]
}
export default function App () {
function reducer(state, action) {
switch (action.type) {
case 'increment':
return state + 1
case 'decrement':
return state - 1
default:
return state
}
}
const [cnt, dispatch] = useReducer(reducer, 0)
return
{cnt}
}
React 表单增强
增强表单处理能力,简化表单处理流程
官网
npm install formik
使用 formik 进行表单数据绑定以及表单提交处理
import { useFormik } from 'formik'
function App () {
const formik = useFormik({initialValues: {username: '张三'}, onSubmit: values => {}})
return
}
const formik = useFormik({
validate: values => {
const errors = {}
if (!values.username) errors.username = '请输入用户名'
return errors
}
})
return
增加 onBlur 事件,显示时先判断 formik.touched.username 是否存在
{formik.touched.username && formik.errors.username ? formik.errors.username : null}
npm install yup
import * as Yup from 'yup'
validationSchema: Yup.object({
username: Yup.string()
.max(15, '用户名的长度不能大于15')
.required('请输入用户名'),
password: Yup.string()
.max(20, '密码的长度不能大于20')
.required('请输入密码')
})
{
...formik.getFieldProps('username')}
import { Formik, Form, Field, ErrorMessage } from 'formik'
import * as Yup from 'yup'
export default function App () {
const initialValues = {
username: '张三',
content: '我是内容',
subject: 'java',
}
const handleSubmit = values => {
console.log(values)
}
const schema = Yup.object({
username: Yup.string().max(15, '用户名的长度不能大于15').required('请输入用户名')
})
return (
)
}
默认情况下,Field 组件渲染的是文本框,如要生成其他表单元素可以使用以下语法
function MyInputField ({ label, ...props }) {
const [field, meta] = useField(props)
return
{ meta.touched && meta.error ? meta.error: null }
}
function Checkbox ({ label, ...props }) {
const [field, meta, helper] = useField(props)
const {value} = meta
const { setValue } = helper
const handleChange = () => {
const set = new Set(value)
if (set.has(props.value)) {
set.delete(props.value)
} else {
set.add(props.value)
}
setValue([...set])
}
return
}
const initialValues = {
hobbies: ['足球', '篮球']
}
集成 CSS 代码在 JS 文件
CSS-IN-JS 是 WEB 项目中将 CSS 代码捆绑在 JS 代码中的解决方案,这种方案旨在解决 CSS 的局限性,例如缺乏动态功能,作用域和可移植性。
CSS-IN-JS 方案的优点:
Emotion 是一个旨在使用 JS 编写 CSS 样式的库
npm install @emotion/core @emotion/styled
通知 babel , 不再需要将 jsx 语法转换为 React.createElement 方法, 而是需要转换为 jsx 方法。
/** @jsx jsx */
import { jsx } from '@emotion/core'
npm run eject
npm install @emotion/babel-preset-css-prop
在 package.json 文件中找到 babel 属性,加入如下内容:
"presets": [
"react-app",
"@emotion/babel-preset-css-prop"
]
现代化 React UI 框架 Chakra-UI
Chakra UI是一个简单的,模块化的易于理解的UI组件库.提供了丰富的构建React应用所需的U|组件.
文档: https://next.chakra-ui.com/docs/getting-started
npm install @chakra-ui/react @emotion/react @emotion/styled framer-motion
Chakra-UI 提供的组件是建立在主题基础之上的,只有先引入了主题组件才能够使用其他组件。
npm install @chakra-ui/theme
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import theme from '@chakra-ui/theme'
import { ChakraProvider } from "@chakra-ui/react"
ReactDOM.render(
,
document.getElementById('root')
);
{
"name": "chakra-ui-guide",
"version": "0.1.0",
"private": true,
"dependencies": {
"@chakra-ui/react": "^1.0.4",
"@chakra-ui/theme": "^1.2.2",
"@emotion/react": "^11.1.3",
"@emotion/styled": "^11.0.0",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"framer-motion": "^3.1.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.1",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
import { Button } from '@chakra-ui/react'
function App() {
return (
);
}
export default App;
Style Props 是用来更改组件样式的,通过为组件传递属性的方式实现。通过传递简化的样式属性一达到提升开发效率的目的。可以在https://chakra-ui.com/docs/features/style-props查看样式简写
Prop | CSS Property | Theme Key |
---|---|---|
m , margin |
margin |
space |
mt , marginTop |
margin-top |
space |
mr , marginRight |
margin-right |
space |
mb , marginBottom |
margin-bottom |
space |
ml , marginLeft |
margin-left |
space |
mx |
margin-left and margin-right |
space |
my |
margin-top and margin-bottom |
space |
p , padding |
padding |
space |
pt , paddingTop |
padding-top |
space |
pr , paddingRight |
padding-right |
space |
pb , paddingBottom |
padding-bottom |
space |
pl , paddingLeft |
padding-left |
space |
px |
padding-left and padding-right |
space |
py |
padding-top and padding-bottom |
space |
如何使用:
import { Box } from '@chakra-ui/react'
function App() {
return (
Box
);
}
export default App;
chakra-ui 提供的组件都支持两种颜色模式,浅色模式(light)和暗色模式(dark).
可以通过 useColorMode 进行颜色模式的更改。
import { Box, Text, Button, useColorMode } from '@chakra-ui/react'
function App() {
const {colorMode, toggleColorMode} = useColorMode() // 注意这里是对象解构,不是数组解构
return (
当前的颜色模式为 {colorMode}
);
}
export default App;
Chakra 将颜色模式存储在 localStorage 中,并使用类名策略来确保颜色模式是持久的
chakra 允许在为元素设置样式时根据颜色模式产生不同值。通过 useColorModeValue 钩子函数实现
import { Box, useColorModeValue } from '@chakra-ui/react'
const bg = useColorModeValue('tomato', 'skyblue')
使组件不受颜色模式的影响,始终保持在某个颜色模式下的样式,使用 LightMode 组件包裹需要作用的组件只显示浅色模式,,使用 DarkMode 组件包裹需要作用的组件只显示暗色模式
import { Button, LightMode } from '@chakra-ui/react'
设置默认颜色模式
通过 theme.config.initialColorMode 可以设置应用使用的默认主题.
使用操作系统所使用的颜色模式
通过 theme.config.useSystemColorMode 可以设置将应用的颜色模式设置为操作系统所使用的颜色模式.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import theme from '@chakra-ui/theme'
import { ChakraProvider } from "@chakra-ui/react"
// 1. 设置默认颜色模式
// theme.config.initialColorMode = 'dark'
// 2. 使用操作系统所使用的颜色模式
theme.config.useSystemColorMode = true
ReactDOM.render(
,
document.getElementById('root')
);
在设置颜色时,可以但不限于取主题对象中的颜色值
function App() {
return
}
使用 space 可以自定义项目间距。这些间距值可以由 width, height 和 maxWidth, minWidth 等样式引用
配置响应数组值中使用的默认断点。这些值将用于生成移动优先(即最小宽度)的媒体查询
// theme.js
export default {
breakpoints: ["30em", "48em", "62em", "8Øem"] ,
};
import {chakra} from '@chakra-ui/react'
const LaGouButton = chakra("button", {
baseStyle: {
borderRadius: 'lg'
},
sizes: {
sm: {
px: '3', // padding-left/padding-right
py: '1', // padding-right/padding-bottom
fontSize: '12px'
},
md: {
px: '4',
py: '2',
fontSize: '14px'
}
},
variants: { // 风格化样式
primary: {
bgColor: 'blue.500',
color: 'white'
},
danger: {
bgColor: 'red.500',
color: 'white'
}
}
});
LaGouButton.defaultProps = {
size: 'sm',
variant: 'primary'
};
function App() {
return (
按钮
)
}
export default App;
const LaGouButton = {
baseStyle: {
borderRadius: 'lg'
},
sizes: {
sm: {
px: '3', // padding-left/padding-right
py: '1', // padding-right/padding-bottom
fontSize: '12px'
},
md: {
px: '4',
py: '2',
fontSize: '14px'
}
},
variants: { // 风格化样式
primary: {
bgColor: 'blue.500',
color: 'white'
},
danger: {
bgColor: 'red.500',
color: 'white'
}
},
defaultProps: {
size: 'sm',
variant: 'primary'
}
}
export default LaGouButton
c. 在 LaGou 文件夹中创建 index.js 文件用于导入导出所有的自定义组件
import LaGouButton from './Button'
export default {
LaGouButton
}
d. 在 src 文件夹中的 index.js 文件中导入自定义 Chakra-UI 组件并和 components 属性进行合并
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import theme from '@chakra-ui/theme'
import { ChakraProvider } from "@chakra-ui/react"
import LaGouComponents from './LaGou'
const myTheme = {
...theme,
components: {
...theme.components,
...LaGouComponents
}
}
console.log(myTheme)
ReactDOM.render(
,
document.getElementById('root')
);
e. 在组件中使用样式化组件
import {chakra} from '@chakra-ui/react'
const LaGouButton = chakra("button", {
themeKey: 'LaGouButton'
});
function App() {
return (
按钮
)
}
export default App;