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