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);
复制代码
举个例子react
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; 复制代码
举个例子ios
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
,并返回一个更新后的值。git
举个例子github
function Counter() {
const [counter, setCounter] = useState(0);
return (
<> <p>{counter}</p> <button onClick={() => setCounter(counter => counter + 10)}> counter + 10 </button> </> ); } 复制代码
initialState
参数只会在组件的初始化渲染中起做用,后续渲染时会被忽略state
须要经过复杂计算得到,则能够传入一个函数,在函数中计算并返回初始的 state
,此函数只在初始渲染时被调用举个例子ajax
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 组件没有提供这样的方法。即便咱们提取出一个方法,咱们仍是要在两个地方调用它。express
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。redux
为何在组件内部调用 useEffect? 将 useEffect 放在组件内部让咱们能够在 effect 中直接访问 count state 变量(或其余 props)。咱们不须要特殊的 API 来读取它 —— 它已经保存在函数做用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的状况下,还引入特定的 React API。axios
useEffect 会在每次渲染后都执行吗? 是的,默认状况下,它在第一次渲染以后和每次更新以后都会执行。(咱们稍后会谈到如何控制它)你可能会更容易接受 effect 发生在“渲染以后”这种概念,不用再去考虑“挂载”仍是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。api
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 尺寸屏幕下
至此呢,效果还算不错。
虽然效果还不错,可是 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
的内容但愿读者去官方网站进行更深刻的学习。