前言
组件通信概念
- 组件之间 的 数据传递 ,根据组件嵌套关系的不同,有不同的通信方法(如下图所示);
一、父子通信
1.1 父传子 - props
- 将 父组件 App 中的 name数据 传递给 他的子组件 Son;
1.1.1 实现步骤
- 父组件 传递 数据 :
- 在 子组件标签 上 绑定属性;
- 属性名自定义;
- 子组件 接收 数据 :
- 子组件内部 通过
props
参数 接收数据; - 子组件函数的形参位置接收参数;
- 该
props
是个对象 ,里面包含了 父组件 传递的 所有数据; - 要使用传递的数据,可以通过 点语法 或者 解构 使用;
- 子组件内部 通过
1.1.2 props说明
props
可传递 任意类型 的 数据 :- 数字、字符串、布尔值、数组、对象、函数、JSX;
props
是 只读对象 :- 子组件 只能读取
props
中的数据,不能直接修改; - 父组件 的 数据 只能由 父组件 修改;
- 子组件 只能读取
1.1.3 父传子 - 特殊的 props children
- 场景 :
- 当我们把 内容嵌套 在 子组件标签中时 ,父组件会自动在名为
children
的prop
属性中 接收该内容;
- 当我们把 内容嵌套 在 子组件标签中时 ,父组件会自动在名为
1.2 子传父
- 点击 send按钮 的时候,将 Son组件 的 msg数据 传递给 父组件 App;
- 实现思路 :
- 在 子组件 中 调用 父组件中的函数 并 传递参数;
- 代码展示:
- 注意 :
- 如果传递给子组件的是函数,一般都是以
on
开头的;(不以这个开头也没关系,看个人习惯);
二、使用状态提升实现兄弟组件通信
- 点击 A组件 的 send 按钮的时候,要将 A组件 的 name数据 发送给它的 兄弟组件 B;
- 实现思路 :
- 借助 "状态提升" 机制,通过 共同的父组件 进行 兄弟组件之间 的 数据传递;
- 状态提升 :
- 将需要使用的状态提升到他们共同的父组件中;
-
代码展示:
jsximport { useState } from 'react'; const App = () => { const [brotherDate, setBrotherDate] = useState(''); return ( <div> <div> 父组件接收的数据(子组件触发条件之后才能传递过来): <span style={{ color: 'red', fontSize: '20px' }}>{brotherDate}</span> </div> <br /> <A bDate={brotherDate} onUpdateBrotherDate={(val) => setBrotherDate(val)} /> <br /> <B aDate={brotherDate} onUpdateBrotherDate={(val) => setBrotherDate(val)} /> </div> ); }; const A = ({ bDate, onUpdateBrotherDate }) => { const [val, setVal] = useState('this is A name'); return ( <div> <div>A组件: A向B传递的数据: {val}</div> <div> A组件: B传递的数据: <span style={{ color: 'red', fontSize: '20px' }}>{bDate}</span> </div> <input type="text" value={val} onChange={(e) => setVal(e.target.value)} /> <button onClick={() => onUpdateBrotherDate(val)}>A组件数据 ➡ B组件</button> </div> ); }; const B = ({ aDate, onUpdateBrotherDate }) => { const [val, setVal] = useState('this is B name'); return ( <div> <div> B组件: A传递过来的数据: <span style={{ color: 'red', fontSize: '20px' }}>{aDate}</span> </div> <div>B组件: B向A传递的数据: {val}</div> <input type="text" value={val} onChange={(e) => setVal(e.target.value)} /> <button onClick={() => onUpdateBrotherDate(val)}>B组件数据 ➡ A组件</button> </div> ); }; export default App;
-
效果展示:
三、使用Context机制跨层级组件通信
- 需要将 App组件 中的数据,传递给 B组件,他们之间是隔着一层A组件的,使用
context
机制进行数据的传递;
- 实现步骤 :
- 使用
createContext
方法创建一个 上下文对象Ctx
(上下文对象的名字可以随便起); - 在 顶层组件 (App)中通过
Ctx.Provider
组件 (高阶组件) 提供数据; - 在 底层组件 (B)中通过
useContext
钩子函数 使用数据;
- 使用
- ❗ 注意 :
Ctx.Provider
组件上有个value
属性,用来提供数据,属性名只能是value
;- 一定要使用
Ctx.Provider
组件 包裹住 层级组件 ,否则在底层组件使用useContext
钩子得到的数据就是undefined
;
-
代码展示:
jsximport { useContext } from 'react'; import { createContext } from 'react'; // 1、使用 createContext 创建上下文对象 const MsgContext = createContext(); const A = () => { return ( <div> <h2>this is A Component</h2> <B /> </div> ); }; const B = () => { // 3、在底层组件,使用 useContext 钩子函数获取数据 const msg = useContext(MsgContext); return ( <div> <h3>this is B Component</h3> {msg} </div> ); }; const App = () => { const msg = 'this is app msg'; // 2、在顶层组件,通过 Provider将数据传递给底层组件 return ( <div> {/* value属性提供数据,不能更改属性名,只能使用value */} {/* 一定要使用 MsgContext.provider 组件包裹住层级组件 */} <MsgContext.Provider value={msg}> <h1>this is App Component</h1> <A /> </MsgContext.Provider> </div> ); }; export default App;
-
演示效果:
四、案例展示 - B站评论 - 组件版
- css文件请移步至上篇文章的末尾自取;
- 效果展示:
- 功能需求:
- 渲染评论列表;
- 实现删除评论;
- 只有自己的评论才显示删除按钮;
- 点击删除按钮,删除当前评论,列表中不再显示;
- 渲染导航Tab和高亮实现;
- 评论列表排序功能实现;
- 最新:评论列表按照创建时间排序(新的在前);
- 最热:点赞数排序(点赞数多的在前);
- 实现评论功能;
- 点击发布之后,需要清空输入框的内容,并且自动获取焦点;
- 回车也可以发送评论;
- 核心思路:
- 使用
useState
维护评论列表;- 使用
map
对列表进行遍历渲染(一定要添加key
);- 将代码拆分成组件形式;
- 使用组件通信;
-
代码展示:
jsximport { createContext, useContext, useRef, useState } from 'react'; import _ from 'lodash'; import './bilibili-review.scss'; import avatar from './images/bozai.png'; // 评论列表数据 const defaultList = [ { // 评论id rpid: 3, // 用户信息 user: { uid: '13258165', avatar: 'https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/comment/zhoujielun.jpeg', uname: '周杰伦' }, // 评论内容 content: '哎哟,不错哦', // 评论时间 ctime: '2023-10-18 08:15', // 喜欢数量 like: 98, // 0:未表态 1: 喜欢 2: 不喜欢 action: 0 }, { rpid: 2, user: { uid: '36080105', avatar: 'https://yjy-teach-oss.oss-cn-beijing.aliyuncs.com/reactbase/comment/xusong.jpeg', uname: '许嵩' }, content: '我寻你千百度 日出到迟暮', ctime: '2023-11-13 11:29', like: 88, action: 2 }, { rpid: 1, user: { uid: '30009257', avatar, uname: '黑马前端' }, content: '学前端就来黑马', ctime: '2023-10-19 09:00', like: 66, action: 1 } ]; // 当前登录用户信息 const user = { // 用户id uid: '30009257', // 用户头像 avatar, // 用户昵称 uname: '黑马前端' }; const MsgContext = createContext(); // 头部导航栏 const HeaderTab = ({ list, updateList }) => { const tabOptions = [ { type: 'latest', text: '最新' }, { type: 'hottest', text: '最热' } ]; let [activeTab, setActiveTab] = useState('latest'); /** 切换Tab栏 */ const updateActiveTab = (type) => { setActiveTab(type); updateList(type); }; return ( <div className="header align-center"> <div className="title align-center"> 评论<em>{list.length}</em> </div> <ul className="tab align-center"> {tabOptions.map((item, index) => { return ( <li className={`item align-center ${activeTab === item.type && 'active'}`} key={item.type} onClick={() => updateActiveTab(item.type)}> <span>{item.text}</span> {tabOptions.length - 1 !== index && <span className="split-line"></span>} </li> ); })} {/* // 多类名写法 // className={['item', 'align-center', activeTab === item.type ? 'active' : null].join(' ')} // className={['item', 'align-center', activeTab === item.type && 'active'].join(' ')} // className={`item align-center ${activeTab === item.type ? 'active' : null}`} // className={`item align-center ${activeTab === item.type && 'active'}`} */} </ul> </div> ); }; // 评论输入框 const PostReview = ({ updateList }) => { // 受控表单绑定 const [content, setContent] = useState(''); // 输入框ref const inputRef = useRef(null); /** 发布评论 */ const addReviewItem = (e) => { if (e.key === 'Enter') e.preventDefault(); if (!content) return alert('请输入评论!'); const time = new Date().toLocaleString(); const m_d = time.split(' ')[0].split('/'); const h_m = time.split(' ')[1].split(':'); h_m.pop(); const obj = { rpid: new Date().getTime(), user, content, ctime: `${m_d.join('-')} ${h_m.join('-')}`, like: 0, action: 0 }; updateList('add', '', obj); setContent(''); // 自动获取焦点 inputRef.current.focus(); }; return ( <div className="post-review align-center"> <img src={user.avatar} alt={user.uname} className="avatar" /> <div className="input align-center"> <textarea type="text" ref={inputRef} value={content} onChange={(e) => setContent(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && addReviewItem(e)} placeholder="发一条友善的评论" /> <button onClick={addReviewItem}>发布</button> </div> </div> ); }; // 一条评论 const ReviewItem = ({ item }) => { const updateList = useContext(MsgContext); return ( <div className="review-item"> <div className="left"> <img src={item.user.avatar} alt={item.user.uname} /> </div> <div className="right"> <div className="user-name">{item.user.uname}</div> <div className="content">{item.content}</div> <div className="bottom"> <div className="time">{item.ctime}</div> <div className="like-num">点赞数:{item.like}</div> <ul className="controls"> {item.user.uid === user.uid && ( <li className="del" onClick={() => updateList('del', item.rpid)}> 删除 </li> )} </ul> </div> </div> </div> ); }; // 评论列表 const ReviewList = ({ list }) => { return ( <div className="review-list"> {list.map((item) => { return <ReviewItem item={item} key={item.rpid} />; })} </div> ); }; const App = () => { // 评论数据 const [list, setList] = useState(_.orderBy(defaultList, 'ctime', 'desc')); /** 更新评论列表:排序 + 删除 + 新增 */ const updateList = (type, id, newReview = {}) => { let newList = []; switch (type) { case 'latest': newList = _.orderBy(list, 'ctime', 'desc'); break; case 'hottest': newList = _.orderBy(list, 'like', 'desc'); break; case 'del': newList = list.filter((item) => item.rpid !== id); break; case 'add': newList = [newReview, ...list]; break; default: newList = list; break; } setList(newList); }; return ( <div className="app"> <HeaderTab list={list} updateList={updateList} /> <div className="review-box"> <PostReview updateList={updateList} /> {/* 使用 context 机制实现跨层通信 */} <MsgContext.Provider value={updateList}> <ReviewList list={list} /> </MsgContext.Provider> </div> </div> ); }; export default App;