React18 (三) React組件,props 和state

1. React組件

在React中網頁被拆分爲了一個個的組件,組件是獨立可複用的代碼片段。具體來說,組件可能是頁面中的一個按鈕,一個對話框,一個彈出層等。React中定義組件的方式有兩種:基於函數的組件和基於類的組件。

1.1 函數組件

如下面代碼,定義一個箭頭函數,然後是export default導出即可。

import BackDrop from '../BackDrop/BackDrop';
import './ConfirmModal.css';

const ConfirmModal = (props) => {
    return <BackDrop>
        <div className="confirmModal">
            <div className="confirmText">
                <p>{props.confirmText}</p>
            </div>
            <div className="confirmButton">
                <button onClick={props.onOk}>Sure</button>
                <button onClick={props.onCancel}>Cancel</button>
            </div>
        </div>
    </BackDrop>;
};

export default ConfirmModal;

1.2 類組件

類組件相比函數組件使用的較少,其通多依賴React.Component組件,通過render()方法渲染組件

import React from 'react';
import './Like.css';

class Like extends React.Component {
    state ={
        kawa:132
    };
    comRef = React.createRef();
    cli = () => {
        console.log(this.comRef.current);
    }
    render() { 
        console.log(this.props); 
       
        return <div ref={this.comRef}>
            <div>Like-{this.state.kawa}</div>
            <button onClick={this.cli}>Print Log</button>
        </div>;
    }
}
 
export default Like;

2. props

在開發時,我們往往需要的是一些動態顯示的組件,換句話組件中所顯示的內容必須是動態設置的。

在使用組件時,可以通過向組件傳遞參數的形式來向組件傳遞數據,這一點和JS中的函數非常相似。函數可以通過傳遞實參來決定函數的執行結果,組件也不例外。函數的參數如何傳遞我們是非常清楚的,那麼組件的參數是怎麼傳遞的呢?組件的參數需要通過屬性傳遞,可以像這樣向組件中傳遞參數:

import LogsItem from './logsItem/LogsItem';
import './LearnLogs.css';
import LogFilter from '../logFilter/LogFilter';
import { useState } from 'react';

const LearnLogs = (props) => {

    const [year, setYear] = useState(2022);

    let filterData = props.items.filter(item => { 
        const filterYear = typeof item.date === "string" ? new Date(item.date).getFullYear() : item.date.getFullYear();
        return filterYear === year
    });

    const changeFilter = (year) => {
        setYear(year);
    };

    return <>
        <LogFilter onChangeFilter={changeFilter} year={year}></LogFilter>
        {filterData.length ? filterData.map((item) => <LogsItem
            key={item.id}
            date={item.date}
            desc={item.desc}
            time={item.time}
            onDelLog={() => props.onDelLog(item.id)} />) : <div className="emptyLogs">No Learning Record</div>}
    </>;
};

export default LearnLogs;

上邊的<LogItem>上添加了屬性key,date,desc和time,這些屬性會被封裝到一個對象中並作爲參數傳遞給LogItem組件,只需要在LogItem組件中定義一個參數即可獲取,通常這個參數我們會命名爲props,如下面代碼:

import { useState } from 'react';
import ConfirmModal from '../../UI/ConfirmModal/ConfirmModal';
import Like from './like/Like';
import LogDate from './logDate/LogDate';
import './LogsItem.css';

const LogsItem = (props) => {

    const [showConfirm, setShowConfirm] = useState(false);

    const removeItem = () => {
        setShowConfirm(true);

    };

    const cancelConfirmModal = () => {
        setShowConfirm(false);
    }

    const okConfirmModal = () => {
        props.onDelLog();
        setShowConfirm(false);
    }

    return <div className="item">
        
        {showConfirm && <ConfirmModal confirmText={"Do you want to delete this record ?"} onCancel={cancelConfirmModal} onOk={okConfirmModal}/>}
        <LogDate date={props.date} />
        <div className="content">
            <h2 className="desc">
                {props.desc}
            </h2>
            <div className="time">{props.time} mins</div>
        </div>
        <div className="like">
            <Like {...props} />
        </div>
        <button className="btn removeBtn" onClick={removeItem}>X</button>
    </div>;
};

export default LogsItem;

在組件內部可以通過props.xxx來訪問外部傳遞進的屬性,從而達到動態設置的目的。需要注意的是,標籤體也可以設置爲props的一個屬性,叫做children,可以通過props.children來獲取標籤體的內容。還有一點一定要記住,props中的屬性是隻讀屬性是無法修改的

3. state

props中的所有屬性都是不可變的。但在實際的開發中,我們更希望的是數據發生變化時,頁面也會隨着數據一起變化。React爲我們提供了state用來解決這個問題。

state和props類似,都是一種存儲屬性的方式,但是不同點在於state只屬於當前組件,其他組件無法訪問。並且state是可變的,當其發生變化後組件會自動重新渲染,以使變化在頁面中呈現。

state也可以被認爲是一個變量,但是它的定義方式不太一樣,我們以函數組件爲例來介紹state的使用方式(類組件咱們後邊再說)。在函數中使用state我們需要使用一種鉤子(hook)函數。鉤子函數可以在函數組件中“勾出”React的特性,換句話說我們要用一個函數“勾出”state。語法:

const [state, setState] = useState(initialState);

下面是一個demo代碼

import { useState } from 'react';
import LearnLogs from './Components/learnLogs/LearnLogs';
import LogForm from './Components/logForm/LogForm';
import './App.css';

const App = () => {

    console.log("<init APP>");

    const [items, setItems] = useState([
        {
            id: 100001,
            date: new Date(2021, 1, 20, 18, 20),
            desc: 'Learn Apache',
            time: '45'
        },
        {
            id: 10002,
            date: new Date(2022, 3, 17, 18, 20),
            desc: 'Learn SpringBoot',
            time: '25'
        },
        {
            id: 100003,
            date: new Date(2022, 4, 22, 18, 20),
            desc: 'Learn Redis',
            time: '75'
        },
        {
            id: 100005,
            date: new Date(2022, 7, 20, 18, 20),
            desc: 'Learn Kafka',
            time: '90'
        }
    ])

    const saveLogForm = (formData) => {
        formData = { ...formData, id: Date.now() + '' }
        // items.push(formData);
        // setItems(items);
        setItems([formData, ...items]);
    }

    const delLogById = (itemId) => {
        console.log("delete item id: ", itemId);
        const newItems = items.filter(item => item.id !== itemId);
        setItems(newItems);
    }

    return <>
        <LogForm className="card" onSaveLogForm={saveLogForm}></LogForm>
        <div className="learn-logs">
            <LearnLogs items={items} onDelLog={delLogById} />
        </div>
    </>;
};

export default App;

通過鉤子函數useState()勾出state,useState()中需要傳遞一個初始值,這個值就是你希望在變量中存儲的值。函數會返回一個數組,數組中有兩個元素,第一個元素是存儲了值的變量,第二個元素是一個函數用來對值進行修改。如:

    const [items, setItems] = useState([
        {
            id: 100001,
            date: new Date(2021, 1, 20, 18, 20),
            desc: 'Learn Apache',
            time: '45'
        },
        {
            id: 10002,
            date: new Date(2022, 3, 17, 18, 20),
            desc: 'Learn SpringBoot',
            time: '25'
        },
        {
            id: 100003,
            date: new Date(2022, 4, 22, 18, 20),
            desc: 'Learn Redis',
            time: '75'
        },
        {
            id: 100005,
            date: new Date(2022, 7, 20, 18, 20),
            desc: 'Learn Kafka',
            time: '90'
        }
    ])

使用useState()“勾出”的變量就是一個普通變量,它裏邊存儲了初始化的值,這個變量和其他變量沒什麼大區別,同樣修改這個變量的值也不會對組件產生實質性的影響,所以不要嘗試直接爲state賦值。useState()“勾出”的函數用來修改state的值,他需要一個新的state值作爲參數,調用後會觸發組件的重新渲染,從而使得頁面刷新,在每次的重新渲染中都會使用新的state值作爲參數。如:

    const saveLogForm = (formData) => {
        formData = { ...formData, id: Date.now() + '' }
        setItems([formData, ...items]);
    }

    const delLogById = (itemId) => {
        console.log("delete item id: ", itemId);
        const newItems = items.filter(item => item.id !== itemId);
        setItems(newItems);
    }

不管是添加和刪除item,都是通過setItems()來重新賦值

4.Ref獲取DOM

在React中爲我們提供了可以直接訪問原生DOM對象的方式。ref就是幹這個事的。ref是reference的簡寫,換句話說就是用來獲取真實DOM對象的引用。我們要獲取元素的真實DOM對象,首先我們需要使用useRef()這個鉤子函數獲取一個對象,這個對象就是一個容器,React會自動將DOM對象傳遞到容器中。代碼const divRef = useRef()就是通過鉤子函數在創建這個對象,並將其存儲到變量中。創建對象後,還需要在被獲取引用的元素上添加一個ref屬性,該屬性的值就是剛剛我們所聲明的變量,像是這樣ref={divRef}這句話的意思就是將對象的引用賦值給變量divRef。這兩個步驟缺一不可,都處理完了,就可以通過divRef來訪問原生DOM對象了。

import React from 'react';
import './Like.css';


class Like extends React.Component {

    state ={
        kawa:132
    };

    comRef = React.createRef();


    cli = () => {
        console.log(this.comRef.current);
    }

    render() { 
        console.log(this.props); 
       
        return <div ref={this.comRef}>
            <div>Like-{this.state.kawa}</div>
            <button onClick={this.cli}>Print Log</button>
        </div>;
    }
}
 
export default Like;

如上面的代碼,點擊“Print Log”按鈕後,出發cli函數,打印目標DOM對象

上例中,如果想訪問div的原生DOM對象,只需通過comRef.current即可訪問,它可以調用DOM對象的各種方法和屬性,但還是要再次強調:慎用!儘量減少在React中操作原生的DOM對象,如果實在非得操作也儘量是那些不會對數據產生影響的操作,像是設置焦點、讀取信息等。useRef()所返回的對象就是一個普通的JS對象,所以上例中即使我們不使用鉤子函數,僅僅創建一個形如{current:null}的對象也是可以的。只是我們自己創建的對象組件每次渲染時都會重新創建一個新的對象,而通過useRef()創建的對象可以確保組件每次的重渲染獲取到的都是相同的對象。

5.protal

React元素中的子組件,在DOM中也會是其父組件對應DOM的後代元素。但是,在有些場景下如果將子組件直接渲染爲父組件的後代,在網頁顯示時會出現一些問題。比如,需要在React中添加一個會蓋住其他元素的Backdrop組件,Backdrop顯示後,頁面中所有的元素都會被遮蓋。通過ReactDOM中的createPortal()方法,可以在渲染元素時將元素渲染到網頁中的指定位置。這個方法就和他的名字一樣,給React元素開啓了一個傳送門,讓它可以去到它應該去的地方。

Portal的用法

1.在index.html中添加一個新的元素
2.在組件中中通過ReactDOM.createPortal()將元素渲染到新建的元素中

在index.html中添加新元素:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Item List</title>
</head>
<body>
    <div id="root"></div>
    <div id="backdrop-root"></div>
</body>
</html>

添加Backdrop組件:

import ReactDOM from 'react-dom';
import './BackDrop.css';

const backdropRoot = document.getElementById("backdrop-root");

const BackDrop = (props) => {

    return ReactDOM.createPortal(<div className="backDrop">
        {props.children}
    </div>, backdropRoot);
}

export default BackDrop;

顯示的效果如下

6.Fragment

在React中,JSX必須有且只有一個根元素。這就導致了在有些情況下我們不得不在子元素的外部添加一個額外的父元素,像是這樣:

import { useState } from 'react';
import LearnLogs from './Components/learnLogs/LearnLogs';
import LogForm from './Components/logForm/LogForm';
import './App.css';

const App = () => {

    console.log("<init APP>");

    const [items, setItems] = useState([
        {
            id: 100001,
            date: new Date(2021, 1, 20, 18, 20),
            desc: 'Learn Apache',
            time: '45'
        },
        {
            id: 10002,
            date: new Date(2022, 3, 17, 18, 20),
            desc: 'Learn SpringBoot',
            time: '25'
        },
        {
            id: 100003,
            date: new Date(2022, 4, 22, 18, 20),
            desc: 'Learn Redis',
            time: '75'
        },
        {
            id: 100005,
            date: new Date(2022, 7, 20, 18, 20),
            desc: 'Learn Kafka',
            time: '90'
        }
    ])

    const saveLogForm = (formData) => {
        formData = { ...formData, id: Date.now() + '' }
        // items.push(formData);
        // setItems(items);
        setItems([formData, ...items]);
    }

    const delLogById = (itemId) => {
        console.log("delete item id: ", itemId);
        const newItems = items.filter(item => item.id !== itemId);
        setItems(newItems);
    }

    return <div>
        <LogForm className="card" onSaveLogForm={saveLogForm}></LogForm>
        <div className="learn-logs">
            <LearnLogs items={items} onDelLog={delLogById} />
        </div>
    </div>;
};

export default App;

上邊這段代碼中,組件內部需要引入兩個組件 LogForm 和被div嵌套的LearnLogs。由於是兩個組件,根據JSX的語法必須在兩個組件的外部在套一個div才能夠正常使用,但是這個外層的div在最終的頁面中沒有任何的實質性作用。

遇到這種情況我們就非常希望能有一種方式可以引入多個組件,但又不會在最終的網頁中添加多餘的結構,那麼我們可以定義這樣一個組件:

實際上在React中已經爲我們提供好了一個現成的組件幫助我們完成這個工作,這個組件可以通過React.Fragment使用,上述案例,也可以修改成這樣:

    return <Fragment>
        <LogForm className="card" onSaveLogForm={saveLogForm}></LogForm>
        <div className="learn-logs">
            <LearnLogs items={items} onDelLog={delLogById} />
        </div>
    </Fragment>;
在React中爲我們提供了一種更加便捷的方式,直接使用<></>代替Fragment更加簡單:
    return <>
        <LogForm className="card" onSaveLogForm={saveLogForm}></LogForm>
        <div className="learn-logs">
            <LearnLogs items={items} onDelLog={delLogById} />
        </div>
    </>;

可以看到app這組件沒有額外的包裹DOM元素

代碼地址:https://github.com/showkawa/react18-ZeroToOne/tree/main/react02

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章