-
Hook
useState()- 一个Hook,用于在函数组件中添加状态 State
-
const [count, setCount] = useState(0)表示const [当前状态值,更新状态值的函数] = 定义初始值为0 - 每次调用setCount组件都会重新渲染
-
useState()可以用于任何数据类型,字符串、对象、数组等,一个组件里也可以使用多个useState()来管理不同的状态 -
useState()不会自动合并对象或数组,更新时需要手动保留原来的值,例如更新一个user对象,setUser({...user, {name: 'Angel'}}),先使用展开运算符展开对象再更新name字段 -
useState()的更新是异步的,当我们想要连续更新时,例如想让count连续加一,写2遍setCount(count + 1)是没用的,正确方式应该是setCount(prev => prev + 1)
-
Hook
useEffect()-
useEffect(fn, [])依赖项也就是数组为空时,表示在组件挂载时componentDidMount执行一次,常用于初始化数据 -
useEffect(fn, [x])依赖项有值时,监听x,依赖项更新时执行,类似componentDidUpdate -
useEffect(fn)所有更新都会执行,componentDidMount + DidUpdate -
return () => {},清理副作用,componentWillUnmount
-
-
自定义Hook
src/hooks/use-intersection-observer.ts- 一个以
use开头的普通Js函数,内部可调用react的其他Hook,Hook的主要作用是:复用逻辑 - 当前hook的功能是观察一个目标元素是否进入视口并返回目标元素和是否进入视口的布尔值
- 这个hook使用了
IntersectionObserver API来判断一个DOM元素是否出现在视口中,适合用在懒加载、滚动触发动画等场景,请点击查看 mdn - IntersectionObserver -
IntersectionObserver是浏览器提供的一个原生API,用于监听一个元素是否进入或离开某个滚动容器 -
const observer = new IntersectionObserver(callback, options),callback表示当被观察元素进入或离开视口时的回调函数,options是配置选项,可以定义视口的边界和触发条件,如下:- root:
- DOM元素或者null,视口的容器元素,默认是浏览器视口
- threshold:
- 数字或数组,默认为0,触发回调的可见比例
- 0.5表示被观察元素至少50%出现在视口时,触发回调
- 取值范围在0-1之间,也可以是个数组[0, 0.5, 1]表示在多个可见比例时触发回调
- rootMargin:
- 字符串,扩大或收缩视口边界,用于提前/延迟触发,如'100px','-100px'
- 偏移量可用px或%来表达
- root:
- 一个以
import { useEffect, useRef, useState } from 'react'
// options 是IntersectionObserver的初始化配置
export const useIntersectionObserver = ( options?: IntersectionObserverInit ) => {
const targetRef = useRef<HTMLDivElement>(null)
const [isIntersecting, setIsIntersecting] = useState(false)
useEffect(() => {
// 被观察者默认是个数组,我们这里只观察一个元素,所以[entry]解构取第一个
const observer = new IntersectionObserver(( [entry] ) => {
setIsIntersecting(entry.isIntersecting)
}, options)
if (targetRef.current) observer.observe(targetRef.current)
// 组件卸载时,取消观察
return () => observer.disconnect()
}, [options])
return { targetRef, isIntersecting}
}
-
可复用组件
src/components/infinite-scroll.tsx- 这个组件的基本思路是将组件本身作为一个哨兵元素,放在列表末尾,当哨兵进入视口,触发方法
- 通过
ref={targetRef}将当前元素赋值给hook的targetRef.current作为被观察者
const { targetRef, isIntersecting } = useIntersectionObserver({
// 观察区域的边距,把视口向四周扩大100px
// 元素还没真正进入视口只是进入扩大后的区域,也会触发回调
// 用于懒加载时可以提前加载内容
rootMargin: '100px',
// 被观察元素有至少50%出现在视口时,IntersectionObserver的回调被触发
// 取值范围在0-1之间,也可以是个数组[0, 0.5, 1]表示在多个可见比例时触发回调
threshold: 0.5
})
-
获取分页数据
useSuspenseInfiniteQuery()- 上一篇讲到使用该hook来在Suspense模式下获取分页数据
const [videos, query] = trpc.studio.getMany.useSuspenseInfiniteQuery( // 接口定义的参数 { limit: DEFAULT_LIMIT }, // React Query的配置项 // lastPage 指返回的分页数据 // nextCursor 下一次分页请求需要发送的标记 { getNextPageParam: lastPage => lastPage.nextCursor } );-
videos是经过整合后的InfiniteData<T>类型的分页数据,假设我们每次加载5条列表数据,加载2次,那么videos.pages数据结构如下:
[ { videosData: [ {id:1, title...}, {id: 2, title...}... {id:5, title...} ], nextCursor: { id: 5, updatedAt: "..." } }, { videosData: [ {id:6, title...}, {id: 7, title...}... {id:10, title...} ], nextCursor: { id: 10, updatedAt: "..." } } ]-
query是剩下的react-query的返回对象,包含一些辅助方法例如fetchNextPage(),hasNaextPage,isFetchingNextPage -
videos.pages.flatMap( page => page.videosData )处理后得到的是合并后的所有数据
[ { id: '1', title: '视频1'... }, { id: '2', title: '视频2'... }, { id: '3', title: '视频3'... }, { id: '4', title: '视频4'... }, ... ] -
Tips
- 关于列表整行可点击跳转
- 首先,列表中是无法使用
<Link>进行跳转的,Next.js 的<Link>必须生成<a>,而 HTML 规范不允许<a>直接包裹<tr> - shadcn 的
<TableRow>本质就是个<tr>,不能被<Link>包裹起来 - 如果你只想单元格点击跳转,可以在
<TableCell>里放<Link> - 我们这里的解决办法是
onClick + router.push
- 首先,列表中是无法使用
<TableRow className='cursor-pointer' key={ video.id } onClick={ () => router.push(`/studio/videos/${video.id}`) } > ... ... </TableRow> - 关于列表整行可点击跳转