React
一直都提倡使用函数组件,但是有时候需要使用 state
或者其他一些功能时,只能使用类组件,因为函数组件没有实例,没有生命周期函数,只有类组件才有。Hooks
是 React 16.8
新增的特性,它可以让你在不编写 class
的情况下使用 state
以及其他的 React
特性。state
,以前的做法是必须将其它转化为 class
。现在你可以直接在现有的函数组件中使用 Hooks
。use
开头的 React API
都是 Hooks
。render props
(渲染属性)或者 HOC
(高阶组件),但无论是渲染属性,还是高阶组件,都会在原先的组件外包裹一层父容器(一般都是 div 元素),导致层级冗余 。componentDidMount
中注册事件以及其他的逻辑,在 componentWillUnmount
中卸载事件,这样分散不集中的写法,很容易写出 Bug
)。this
Ajax
请求、访问原生 DOM
元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都是写在类组件生命周期函数中的。React
假设当我们多次调用 useState
的时候,要保证每次渲染时它们的调用顺序是不变的。state
,React
会 在重复渲染时保留这个 stateuseState
唯一的参数就是初始 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> </> ); } 复制代码
initialState
参数只会在组件的初始化渲染中起作用,后续渲染时会被忽略state
需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state
,此函数只在初始渲染时被调用举个例子
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> </> ); } 复制代码
const App => () => { useEffect(()=>{}) // 或者 useEffect(()=>{},[...]) return <></> } 复制代码
在这个 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> </> ) } } 复制代码
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> </> ) } 复制代码
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> </> ) } 复制代码
// 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); }; }); // ... } 复制代码
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> ); } 复制代码
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> ) } 复制代码
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
来比较新旧 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}/> </> ) } 复制代码
useMemo
& useCallback
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}/> </> ) } 复制代码
React
规定 useEffect
接收的函数,要么返回一个能清除副作用的函数,要么就不返回任何内容。而 async
返回的是 promise
。
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
会「记住」一些值,同时在后续 render
时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo
可能会影响程序的性能。
在使用 useMemo
前,应该先思考三个问题:
useMemo
的函数开销大不大? 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render
都去重新计算。如果你执行的操作开销不大,那么就不需要记住返回值。否则,使用 useMemo
本身的开销就可能超过重新计算这个值的开销。因此,对于一些简单的 JS 运算来说,我们不需要使用 useMemo
来「记住」它的返回值。Hook
时,返回值一定要保持引用的一致性。 因为你无法确定外部要如何使用它的返回值。如果返回值被用做其他 Hook
的依赖,并且每次 re-render
时引用不一致(当值相等的情况),就可能会产生 bug。所以如果自定义 Hook 中暴露出来的值是 object、array、函数等,都应该使用 useMemo
。以确保当值相同时,引用不发生变化。TypeScript
是 JavaScript
的一个超集,主要提供了类型系统和对 ES6
的支持。
了解了 React Hooks 和 TypeScript,接下来就一起看一下二者的结合实践吧!😄
本实践来源于本人正在开发的开源组件库项目 Azir Design中的 Grid 栅格布局组件。
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
className | 类名 | string | - |
style | Row组件样式 | object:CSSProperties | - |
align | 垂直对齐方式 | top|middle|bottom | top |
justify | 水平排列方式 | start|end|center|space-around|space-between | start |
gutter | 栅格间隔,可以写成像素值设置水平垂直间距或者使用数组形式同时设置 [水平间距, 垂直间距] | number|[number,number] | 0 |
属性 | 说明 | 类型 | 默认值 |
---|---|---|---|
className | 类名 | string | - |
style | Col组件样式 | object:CSSProperties | - |
flex | flex 布局属性 | string|number | - |
offset | 栅格左侧的间隔格数,间隔内不可以有栅格 | number | 0 |
order | 栅格顺序 | number | 0 |
pull | 栅格向左移动格数 | number | 0 |
push | 栅格向右移动格数 | number | 0 |
span | 栅格占位格数,为 0 时相当于 display: none | number | - |
xs | <576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
sm | ≥576px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
md | ≥768px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
lg | ≥992px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
xl | ≥1200px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
xxl | ≥1600px 响应式栅格,可为栅格数或一个包含其他属性的对象 | number|object | - |
这一实践主要介绍 React Hooks + TypeScript 的实践,不对 CSS 过多赘述。
// 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 进行了描述。
// 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; } 复制代码
// 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; + } 复制代码
// 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 尺寸屏幕下至此呢,效果还算不错。
虽然效果还不错,但是 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%的工作,我们是不是忘了点什么???
我们通过 外层 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
的内容希望读者去官方网站进行更深入的学习。