React Hooks
什么是 Hooks
React
一直都提倡使用函数组件,但是有时候需要使用 state
或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有。
Hooks
是 React 16.8
新增的特性,它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性。
如果你在编写函数组件并意识到需要向其添加一些 state
,以前的做法是必须将其它转化为 class
。现在你可以直接在现有的函数组件中使用 Hooks
。
use
开头的 React API
都是 Hooks
。
Hooks 解决了哪些问题?
状态逻辑难复用
在组件之间复用状态逻辑很难,可能要用到 render props
(渲染属性)或者 HOC
(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余 。
趋向复杂难以维护
在生命周期函数中混杂不相干的逻辑(如:在 componentDidMount
中注册事件以及其他的逻辑,在 componentWillUnmount
中卸载事件,这样分散不集中的写法,很容易写出 Bug
)。
类组件中到处都是对状态的访问和处理,导致组件难以拆分成更小的组件。
this 指向问题
父组件给子组件传递函数时,必须绑定 this
Hooks 优势
能优化类组件的三大问题
能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
副作用的关注点分离
副作用指那些没有发生在数据向视图转换过程中的逻辑,如 Ajax
请求、访问原生 DOM
元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。
常用 Hooks
useState
React
假设当我们多次调用 useState
的时候,要保证每次渲染时它们的调用顺序是不变的。
通过在函数组件里调用它来给组件添加一些内部 state
,React
会 在重复渲染时保留这个 state
useState
唯一的参数就是初始 state
useState
会返回一个数组:一个 state
,一个更新 state
的函数
在初始化渲染期间,返回的状态 state
与传入的第一个参数 initialState
值相同。我们可以在事件处理函数中或其他一些地方调用更新 state
的函数。它类似 class
组件的 this.setState
,但是它不会把新的 state
和旧的 state
进行合并,而是直接替换。
使用方法
const [state, setState] = useState(initialState);
复制代码
举个例子
import React, { useState } from 'react';
function Counter() {
const [counter, setCounter] = useState(0);
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter + 1)}>counter + 1</button>
</>
);
}
export default Counter;
复制代码
每次渲染都是一个独立的闭包
举个例子
function Counter() {
const [counter, setCounter] = useState(0);
function alertNumber() {
setTimeout(() => {
// 只能获取到点击按钮时的那个状态
alert(counter);
}, 3000);
}
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter + 1)}>counter + 1</button>
<button onClick={alertNumber}>alertCounter</button>
</>
);
}
复制代码
函数式更新
如果新的 state
需要通过使用先前的 state
计算得出,那么可以将回调函数当做参数传递给 setState
。该回调函数将接收先前的 state
,并返回一个更新后的值。
举个例子
function Counter() {
const [counter, setCounter] = useState(0);
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter => counter + 10)}> counter + 10 </button>
</>
);
}
复制代码
惰性初始化
举个例子
function Counter4() {
console.log('Counter render');
// 这个函数只在初始渲染时执行一次,后续更新状态重新渲染组件时,该函数就不会再被调用
function getInitState() {
console.log('getInitState');
// 复杂的计算
return 100;
}
let [counter, setCounter] = useState(getInitState);
return (
<>
<p>{counter}</p>
<button onClick={() => setCounter(counter + 1)}>+1</button>
</>
);
}
复制代码
useEffect
effect(副作用):指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生 dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。
副作用操作可以分两类:需要清除的和不需要清除的。
原先在函数组件内(这里指在 React 渲染阶段)改变 dom 、发送 ajax 请求以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性
useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API
useEffect 接收一个函数,该函数会在组件渲染到屏幕之后才执行,该函数有要求:要么返回一个能清除副作用的函数,要么就不返回任何内容
与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。
使用方法
const App => () => {
useEffect(()=>{})
// 或者
useEffect(()=>{},[...])
return <></>
}
复制代码
使用 class 组件实现修改标题
在这个 class 中,我们需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
class Counter extends React.Component{
state = {number:0};
add = ()=>{
this.setState({number:this.state.number+1});
};
componentDidMount(){
this.changeTitle();
}
componentDidUpdate(){
this.changeTitle();
}
changeTitle = ()=>{
document.title = `你已经点击了${this.state.number}次`;
};
render(){
return (
<>
<p>{this.state.number}</p>
<button onClick={this.add}>+</button>
</>
)
}
}
复制代码
参考 React 实战视频讲解:进入学习
使用 useEffect 组件实现修改标题
function Counter(){
const [number,setNumber] = useState(0);
// useEffect里面的这个函数会在第一次渲染之后和更新完成后执行
// 相当于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
document.title = `你点击了${number}次`;
});
return (
<>
<p>{number}</p>
<button onClick={()=>setNumber(number+1)}>+</button>
</>
)
}
复制代码
useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
为什么在组件内部调用 useEffect? 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
清除副作用
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
// 相当于componentDidMount 和 componentDidUpdate
useEffect(()=>{
console.log('开启一个新的定时器')
let timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
// useEffect 如果返回一个函数的话,该函数会在组件卸载和更新时调用
// useEffect 在执行副作用函数之前,会先调用上一次返回的函数
// 如果要清除副作用,要么返回一个清除副作用的函数
// return ()=>{
// console.log('destroy effect');
// clearInterval($timer);
// }
});
// },[]);//要么在这里传入一个空的依赖项数组,这样就不会去重复执行
return (
<>
<input value={text} onChange={(event)=>setText(event.target.value)}/> <p>{number}</p>
<button>+</button>
</>
)
}
复制代码
跳过 Effect 进行性能优化
function Counter(){
let [number,setNumber] = useState(0);
let [text,setText] = useState('');
// 相当于componentDidMount 和 componentDidUpdate
useEffect(()=>{
console.log('useEffect');
let timer = setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[text]);// 数组表示 effect 依赖的变量,只有当这个变量发生改变之后才会重新执行 efffect 函数
return (
<>
<input value={text} onChange={(e)=>setText(e.target.value)}/> <p>{number}</p>
<button>+</button>
</>
)
}
复制代码
使用多个 Effect 实现关注点分离
// class版
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
// ...
复制代码
我们可以发现 document.title 的逻辑是如何被分割到 componentDidMount
和 componentDidUpdate
中的,订阅逻辑又是如何被分割到 componentDidMount
和 componentWillUnmount
中的。而且 componentDidMount
中同时包含了两个不同功能的代码。这样会使得生命周期函数很混乱。
Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React
将按照 effect
声明的顺序依次调用组件中的 每一个 effect
。
// Hooks 版
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// ...
}
复制代码
useContext
const value = useContext(MyContext);
复制代码
接收一个 context
对象(React.createContext 的返回值)并返回该 context
的当前值。当前的 context
值由上层组件中距离当前组件最近的 <MyContext.Provider>
的 value prop 决定。
当组件上层最近的 <MyContext.Provider>
更新时,该 Hook
会触发重渲染,并使用最新传递给 MyContext provider
的 context value
值。即使祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件本身使用 useContext
时重新渲染。
别忘记 useContext 的参数必须是 context 对象本身:
正确: useContext(MyContext)
错误: useContext(MyContext.Consumer)
错误: useContext(MyContext.Provider)
提示如果你在接触 Hook
前已经对 context API
比较熟悉,那应该可以理解,useContext(MyContext)
相当于 class
组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。 useContext(MyContext)
只是让你能够读取 context
的值以及订阅 context
的变化。你仍然需要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context。
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.light}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context! </button>
);
}
复制代码
自定义 Hooks
自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook
有时候我们会想要在组件之间重用一些状态逻辑,之前要么用 render props ,要么用高阶组件,要么使用 redux
自定义 Hook 可以让你在不增加组件的情况下达到同样的目的
Hook 是一种复用状态逻辑的方式,它不复用 state 本身
事实上 Hook 的每次调用都有一个完全独立的 state
function useNumber(){
let [number,setNumber] = useState(0);
useEffect(()=>{
setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[]);
return [number,setNumber];
}
// 每个组件调用同一个 hook,只是复用 hook 的状态逻辑,并不会共用一个状态
function Counter1(){
let [number,setNumber] = useNumber();
return (
<div><button onClick={()=>{
setNumber(number+1)
}}>{number}</button></div>
)
}
function Counter2(){
let [number,setNumber] = useNumber();
return (
<div><button onClick={()=>{
setNumber(number+1)
}}>{number}</button></div>
)
}
复制代码
useMemo、useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
复制代码
在a
和b
的变量值不变的情况下,memoizedCallback
的引用不变。即:useCallback
的第一个入参函数会被缓存,从而达到渲染性能优化的目的。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
复制代码
在a
和b
的变量值不变的情况下,memoizedValue
的值不变。即:useMemo
函数的第一个入参函数不会被执行,从而达到节省计算量的目的。
性能优化
Object.is 浅比较
Hook 内部使用 Object.is
来比较新旧 state
是否相等。
与 class
组件中的 setState
方法不同,如果你修改状态的时候,传的状态值没有变化,则不重新渲染。
与 class
组件中的 setState
方法不同,useState
不会自动合并更新对象。你可以用函数式的 setState
结合展开运算符来达到合并更新对象的效果。
function Counter(){
const [counter,setCounter] = useState({name:'计数器',number:0});
console.log('render Counter')
// 如果你修改状态的时候,传的状态值没有变化,则不重新渲染
return (
<>
<p>{counter.name}:{counter.number}</p>
<button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button>
<button onClick={()=>setCounter(counter)}>++</button>
</>
)
}
复制代码
减少渲染次数
默认情况,只要父组件状态变了(不管子组件依不依赖该状态),子组件也会重新渲染
一般的优化:
类组件:可以使用 pureComponent
;
函数组件:使用 React.memo
,将函数组件传递给 memo
之后,就会返回一个新的组件,新组件的功能:如果接受到的属性不变,则不重新渲染函数。
但是怎么保证属性不会变呢?这里使用 useState
,每次更新都是独立的,const [number,setNumber] = useState(0)
也就是说每次都会生成一个新的值(哪怕这个值没有变化),即使使用了 React.memo ,也还是会重新渲染。
const SubCounter = React.memo(({onClick,data}) =>{
console.log('SubCounter render');
return (
<button onClick={onClick}>{data.number}</button>
)
})
const ParentCounter = () => {
console.log('ParentCounter render');
const [name,setName]= useState('计数器');
const [number,setNumber] = useState(0);
const data ={number};
const addClick = ()=>{
setNumber(number+1);
};
return (
<>
<input type="text" value={name} onChange={(e)=>setName(e.target.value)}/> <SubCounter data={data} onClick={addClick}/>
</>
)
}
复制代码
const SubCounter = React.memo(({onClick,data}) =>{
console.log('SubCounter render');
return (
<button onClick={onClick}>{data.number}</button>
)
})
const ParentCounter = () => {
console.log('ParentCounter render');
const [name,setName]= useState('计数器');
const [number, setNumber] = useState(0);
// 父组件更新时,这里的变量和函数每次都会重新创建,那么子组件接受到的属性每次都会认为是新的
// 所以子组件也会随之更新,这时候可以用到 useMemo
// 有没有后面的依赖项数组很重要,否则还是会重新渲染
// 如果后面的依赖项数组没有值的话,即使父组件的 number 值改变了,子组件也不会去更新
//const data = useMemo(()=>({number}),[]);
const data = useMemo(()=>({number}),[number]);
const addClick = useCallback(()=>{
setNumber(number+1);
},[number]);
return (
<>
<input type="text" value={name} onChange={(e)=>setName(e.target.value)}/> <SubCounter data={data} onClick={addClick}/>
</>
)
}
复制代码
常见问题
useEffect 不能接收 async 作为回调函数
React
规定 useEffect
接收的函数,要么返回一个能清除副作用的函数,要么就不返回任何内容。而 async
返回的是 promise
。
如何在 Hooks 中优雅的 Fetch Data
function App() {
const [data, setData] = useState({ hits: [] });
useEffect(() => {
// 更优雅的方式
const fetchData = async () => {
const result = await axios(
'https://api.github.com/api/v3/search?query=redux',
);
setData(result.data);
};
fetchData();
}, []);
return (
<ul>
{data.hits.map(item => ( <li key={item.id}>
<a href={item.url}>{item.title}</a>
</li>
))} </ul>
);
}
复制代码
不要过度依赖 useMemo
useMemo
本身也有开销。useMemo
会「记住」一些值,同时在后续 render
时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo
可能会影响程序的性能。
在使用 useMemo
前,应该先思考三个问题:
传递给 useMemo
的函数开销大不大? 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render
都去重新计算。如果你执行的操作开销不大,那么就不需要记住返回值。否则,使用 useMemo
本身的开销就可能超过重新计算这个值的开销。因此,对于一些简单的 JS 运算来说,我们不需要使用 useMemo
来「记住」它的返回值。
返回的值是原始值吗? 如果计算出来的是基本类型的值(string、 boolean 、null、undefined 、number、symbol),那么每次比较都是相等的,下游组件就不会重新渲染;如果计算出来的是复杂类型的值(object、array),哪怕值不变,但是地址会发生变化,导致下游组件重新渲染。所以我们也需要「记住」这个值。
在编写自定义 Hook
时,返回值一定要保持引用的一致性。 因为你无法确定外部要如何使用它的返回值。如果返回值被用做其他 Hook
的依赖,并且每次 re-render
时引用不一致(当值相等的情况),就可能会产生 bug。所以如果自定义 Hook 中暴露出来的值是 object、array、函数等,都应该使用 useMemo
。以确保当值相同时,引用不发生变化。
TypeScript
什么是 TypeScript
TypeScript
是 JavaScript
的一个超集,主要提供了类型系统和对 ES6
的支持。
为什么选择 TypeScript
TypeScript 增加了代码的可读性和可维护性
类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用了
可以在编译阶段就发现大部分错误,这总比在运行时候出错好
增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等
TypeScript 非常包容
TypeScript 是 JavaScript 的超集,.js 文件可以直接重命名为 .ts 即可
即使不显式的定义类型,也能够自动做出类型推论
可以定义从简单到复杂的几乎一切类型
即使 TypeScript 编译报错,也可以生成 JavaScript 文件
兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取
TypeScript 拥有活跃的社区
大部分第三方库都有提供给 TypeScript 的类型定义文件
TypeScript 拥抱了 ES6 规范,也支持部分 ESNext 草案的规范
了解了 React Hooks 和 TypeScript,接下来就一起看一下二者的结合实践吧!😄
实践
本实践来源于本人正在开发的开源组件库项目 Azir Design 中的 Grid 栅格布局组件。
目标
API
Row
Col
大展身手
这一实践主要介绍 React Hooks + TypeScript 的实践,不对 CSS 过多赘述。
Step-1 根据 API 来给 Row 组件定义 Prop 的类型
// Row.tsx
+ import React, { CSSProperties, ReactNode } from 'react';
+ import import ClassNames from 'classnames';
+
+ type gutter = number | [number, number];
+ type align = 'top' | 'middle' | 'bottom';
+ type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';
+
+ interface RowProps {
+ className?: string;
+ align?: align;
+ justify?: justify;
+ gutter?: gutter;
+ style?: CSSProperties;
+ children?: ReactNode;
+ }
复制代码
这里我们用到了 TypeScript 提供的基本数据类型、联合类型、接口。
基本数据类型 JavaScript 的类型分为两种:原始数据类型(Primitive data types
)和对象类型(Object types)
。
原始数据类型包括:布尔值
、数值
、字符串
、null
、undefined
以及 ES6 中的新类型 Symbol
。我们主要介绍前五种原始数据类型在 TypeScript 中的应用。
联合类型 联合类型(Union Types)表示取值可以为多种类型中的一种。
类型别名 类型别名用来给一个类型起个新名字。
接口 在 TypeScript 中接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对对象的形状(Shape)进行描述。我们在这里使用接口对 RowProps 进行了描述。
Step-2 编写 Row 组件的基础骨架
// Row.tsx
- import React, { CSSProperties, ReactNode } from 'react';
+ import React, { CSSProperties, ReactNode, FC } from 'react';
import ClassNames from 'classnames';
type gutter = number | [number, number];
type align = 'top' | 'middle' | 'bottom';
type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';
interface RowProps {
// ...
}
+ const Row: FC<RowProps> = props => {
+ const { className, align, justify, children, style = {} } = props;
+ const classes = ClassNames('azir-row', className, {
+ [`azir-row-${align}`]: align,
+ [`azir-row-${justify}`]: justify
+ });
+
+ return (
+ <div className={classes} style={style}>
+ {children}+ </div>
+ );
+ };
+ Row.defaultProps = {
+ align: 'top',
+ justify: 'start',
+ gutter: 0
+ };
+ export default Row;
复制代码
在这里我们使用到了泛型,那么什么是泛型呢?
泛型 泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
function loggingIdentity<T>(arg: T): T {
return arg;
}
复制代码
Step-3 根据 API 来给 Col 组件定义 Prop 的类型
// Col.tsx
+ import React, {ReactNode, CSSProperties } from 'react';
+ import ClassNames from 'classnames';
+
+ interface ColCSSProps {
+ offset?: number;
+ order?: number;
+ pull?: number;
+ push?: number;
+ span?: number;
+ }
+
+ export interface ColProps {
+ className?: string;
+ style?: CSSProperties;
+ children?: ReactNode;
+ flex?: string | number;
+ offset?: number;
+ order?: number;
+ pull?: number;
+ push?: number;
+ span?: number;
+ xs?: ColCSSProps;
+ sm?: ColCSSProps;
+ md?: ColCSSProps;
+ lg?: ColCSSProps;
+ xl?: ColCSSProps;
+ xxl?: ColCSSProps;
+ }
复制代码
Step-4 编写 Col 组件的基础骨架
// Col.tsx
import React, {ReactNode, CSSProperties } from 'react';
import ClassNames from 'classnames';
interface ColCSSProps {
// ...
}
export interface ColProps {
// ...
}
+ type mediaScreen = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
+ function sc(size: mediaScreen, value: ColCSSProps): Array<string> {
+ const t: Array<string> = [];
+ Object.keys(value).forEach(key => {
+ t.push(`azir-col-${size}-${key}-${value[key]}`);
+ });
+ return t;
+ }
+ const Col: FC<ColProps> = props => {
+ const {
+ className,
+ style = {},
+ span,
+ offset,
+ children,
+ pull,
+ push,
+ order,
+ xs,
+ sm,
+ md,
+ lg,
+ xl,
+ xxl
+ } = props;
+
+ const [classes, setClasses] = useState<string>(
+ ClassNames('azir-col', className, {
+ [`azir-col-span-${span}`]: span,
+ [`azir-col-offset-${offset}`]: offset,
+ [`azir-col-pull-${pull}`]: pull,
+ [`azir-col-push-${push}`]: push,
+ [`azir-col-order-${order}`]: order
+ })
+ );
+
+ // 响应式 xs,sm,md,lg,xl,xxl
+ useEffect(() => {
+ xs && setClasses(classes => ClassNames(classes, sc('xs', xs)));
+ sm && setClasses(classes => ClassNames(classes, sc('sm', sm)));
+ md && setClasses(classes => ClassNames(classes, sc('md', md)));
+ lg && setClasses(classes => ClassNames(classes, sc('lg', lg)));
+ xl && setClasses(classes => ClassNames(classes, sc('xl', xl)));
+ xxl && setClasses(classes => ClassNames(classes, sc('xxl', xxl)));
+ }, [xs, sm, md, lg, xl, xxl]);
+
+ return (
+ <div className={classes} style={style}>
+ {children}+ </div>
+ );
+ };
+ Col.defaultProps = {
+ offset: 0,
+ pull: 0,
+ push: 0,
+ span: 24
+ };
+ Col.displayName = 'Col';
+
+ export default Col;
复制代码
在这里 TypeScript
编译器抛出了警告。
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ColCSSProps'.
No index signature with a parameter of type 'string' was found on type 'ColCSSProps'. TS7053
71 | const t: Array<string> = [];
72 | Object.keys(value).forEach(key => {
> 73 | t.push(`azir-col-${size}-${key}-${value[key]}`);
| ^
74 | });
75 | return t;
76 | }
复制代码
翻译过来就是:元素隐式地具有 any
类型,类型 string
不能用于ColCSSProps
的索引类型。那么这个问题该如何结局呢?
interface ColCSSProps {
offset?: number;
order?: number;
pull?: number;
push?: number;
span?: number;
+ [key: string]: number | undefined;
}
复制代码
我们只需要告诉 TypeScript
ColCSSProps
的键类型是 string
值类型为 number | undefined
就可以了。
测试
写到现在,该测试一下代码了。
// example.tsx
import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
return (
<div data-test="row-test" style={{ padding: '20px' }}>
<Row className="jd-share">
<Col style={{ background: 'red' }} span={2}>
123 </Col>
<Col style={{ background: 'yellow' }} offset={2} span={4}>
123 </Col>
<Col style={{ background: 'blue' }} span={6}>
123 </Col>
</Row>
<Row>
<Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col1 </div>
</Col>
<Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div>
</Col>
</Row>
</div>
);
};
复制代码
xs 尺寸屏幕下
lg 尺寸屏幕下
至此呢,效果还算不错。
Step-5 限制 Row 组件的 Children
虽然效果还不错,但是 Row
组件的 Children
可以传递任何元素
// row.tsx
const Row: FC<RowProps> = props => {
// ...
return (
<div className={classes} style={style}>
{children} </div>
);
};
复制代码
这也太随意了吧!如果 Children
中包含了不是 Col
组件的节点的话布局肯定会出问题,我决定在这里限制一下 Row
组件的 Children
类型。
那么该如何去限制呢?有的人会认为,直接 children.map
,根据结构来判断不就可以了吗?这样做是不可取的,React
官方也指出在 children
上直接调用 map
是非常危险的,因为我们不能够确定 children
的类型。那该怎么办呢?React
官方很贴心的也给我们提供了一个 API React.Children
在这之前我们先给 Col
组件设置一个内置属性 displayName
属性来帮助我们判断类型。
// col.tsx
const Col: FC<ColProps> = props => {
// ...
};
// ...
+ Col.displayName = 'Col';
复制代码
然后我们请出因为大哥 React.Children
API。这个 API
可以专门用来处理 Children
。我们给 Row 组件编写一个 renderChildren
函数
// row.tsx
const Row: FC<RowProps> = props => {
const { className, align, justify, children, style = {} } = props;
const classes = ClassNames('azir-row', className, {
[`azir-row-${align}`]: align,
[`azir-row-${justify}`]: justify
});
+ const renderChildren = useCallback(() => {
+ return React.Children.map(children, (child, index) => {
+ try {
+ // child 是 ReactNode 类型,在该类型下有很多子类型,我们需要断言一下
+ const childElement = child as React.FunctionComponentElement<ColProps>;
+ const { displayName } = childElement.type;
+ if (displayName === 'Col') {
+ return child;
+ } else {
+ console.error(
+ 'Warning: Row has a child which is not a Col component'
+ );
+ }
+ } catch (e) {
+ console.error('Warning: Row has a child which is not a Col component');
+ }
+ });
+ }, [children]);
return (
<div className={classes} style={style}>
- {children}+ {renderChildren()} </div>
);
};
复制代码
至此我们已经完成了 80%的工作,我们是不是忘了点什么???
Step-6 锦上添花-gutter
我们通过 外层 margin
+ 内层 padding
的模式来配合实现水平垂直间距的设置。
// row.tsx
import React, {
CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';
// ...
const Row: FC<RowProps> = props => {
- const { className, align, justify, children, style = {} } = props;
+ const { className, align, justify, children, gutter, style = {} } = props;
+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);
// ...
return (
- <div className={classes} style={style}>
+ <div className={classes} style={rowStyle}>
{renderChildren()} </div>
);};// ...export default Row;
复制代码
Row
组件的 margin
已经这设置好了,那么 Col
组件的 padding
该怎么办呢?有两中办法,一是传递 props
、二是使用 context
,我决定使用 context 来做组件通信,因为我并不想让 Col 组件的 props 太多太乱(已经够乱了...)。
// row.tsx
import React, {
CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';
// ...
export interface RowContext {
gutter?: gutter;
}
export const RowContext = createContext<RowContext>({});
const Row: FC<RowProps> = props => {
- const { className, align, justify, children, style = {} } = props;
+ const { className, align, justify, children, gutter, style = {} } = props;
+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);
+ const passedContext: RowContext = {
+ gutter
+ };
// ...
return (
<div className={classes} style={rowStyle}>
+ <RowContext.Provider value={passedContext}>
{renderChildren()}+ </RowContext.Provider>
</div>
);
};
// ...
export default Row;
复制代码
我们在 Row
组件中创建了一个 context
,接下来就要在 Col
组件中使用,并计算出 Col
组件 gutter
对应的 padding
值。
// col.tsx
import React, {
ReactNode,
CSSProperties,
FC,
useState,
useEffect,
+ useContext
} from 'react';
import ClassNames from 'classnames';
+ import { RowContext } from './row';
// ...
const Col: FC<ColProps> = props => {
// ...
+ const [colStyle, setColStyle] = useState<CSSProperties>(style);
+ const { gutter } = useContext(RowContext);
+ // 水平垂直间距
+ useEffect(() => {
+ if (Object.prototype.toString.call(gutter) === '[object Number]') {
+ const padding = gutter as number;
+ if (padding >= 0) {
+ setColStyle(style => ({
+ padding: `${padding / 2}px`,
+ ...style
+ }));
+ }
+ }
+ if (Object.prototype.toString.call(gutter) === '[object Array]') {
+ const [paddingX, paddingY] = gutter as [number, number];
+ if (paddingX >= 0 && paddingY >= 0) {
+ setColStyle(style => ({
+ padding: `${paddingY / 2}px ${paddingX / 2}px`,
+ ...style
+ }));
+ }
+ }
+ }, [gutter]);
// ...
return (
- <div className={classes} style={style}>
+ <div className={classes} style={colStyle}>
{children} </div>
);};// ...export default Col;
复制代码
到这里呢,我们的栅格组件就大功告成啦!我们来测试一下吧!😄
测试
import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
return (
<div data-test="row-test" style={{ padding: '20px' }}>
<Row>
<Col span={24}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col1 </div>
</Col>
</Row>
<Row gutter={10}>
<Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col1 </div>
</Col>
<Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div>
</Col>
</Row>
<Row gutter={10} align="middle">
<Col span={8}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
</Col>
<Col offset={8} span={8}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2 </div>
</Col>
</Row>
<Row gutter={10} align="bottom">
<Col span={4}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
</Col>
<Col span={8}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2 </div>
</Col>
<Col push={3} span={9}>
<div style={{ height: '130px', backgroundColor: '#2170bb' }}>
Col3 </div>
</Col>
<Col span={4}>
<div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
</Col>
<Col span={8}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2 </div>
</Col>
<Col span={8}>
<div style={{ height: '130px', backgroundColor: '#2170bb' }}>
Col3 </div>
</Col>
<Col pull={1} span={3}>
<div style={{ height: '100px', backgroundColor: '#3170bb' }}>
Col2 </div>
</Col>
</Row>
</div>
);
};
复制代码
总结
至此 React Hooks + TypeScript
的实践分享结束了,我这只列举了比较常用 Hooks API
和 TypeScript
的特性,麻雀虽小、五脏俱全,我们已经可以体会到 React Hooks + TypeScript
带来的好处,二者的配合一定会让我们的代码变得既轻巧有健壮。关于 Hooks
和 TypeScript
的内容希望读者去官方网站进行更深入的学习。
评论