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 尺寸屏幕下
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
的內容但願讀者去官方網站進行更深刻的學習。