React 速通笔记

前言

最近刚学完 React,想着把笔记分享给大家,本笔记特别适合从事后端想要学习前端的人。我看视频是黑马最新的 React 视频(黑马程序员前端React18入门到实战视频教程,从react+hooks核心基础到企业级项目开发实战(B站评论、极客园项目等)及大厂面试全通关_哔哩哔哩_bilibili),个人觉得讲得还不错的。想要完整版可以私信我,如果对你有帮助的话就点个赞关注下吧。后面持续分享 Java 相关技术和笔记。

一、React 基础

1. 创建一个 react 项目

1.利用 create-react-app 工具创建一个 react 项目

npx create-react-app project-name
npm start # 启动项目

2.src 目录只保留 App.js 和 index.js 文件

3.精简 App.js 和 index.js 文件

1.1 src 目录下文件的作用

index.js 是项目的入口,从这里开始运行,App 是根组件被 Index.js 导入,最后渲染到 index.html 中 root 节点上

index.js:

// 项目的核心入口 从这里开始运行

// React 必要的两个核心包
import React from 'react';
import ReactDOM from 'react-dom/client';

// 导入项目的根组件
import App from './App';

// 把 App 根组件渲染到 id 为 root 的 dom 节点上
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <App />
);

App:

// 项目的根组件

// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上
function App() {
  return (
    <div className="App">
      this is React App
    </div>
  );
}

export default App;

2. jsx 基础-概念和本质

2.1 JSX 是什么?

JSX 表示在 JS 代码中编写 HTML 模板结构,是 React 中编写 UI 模板的方式

优势:

  1. HTML 的声明式模板写法

  2. JS 的可编程能力

JSX 是 JS 的拓展,浏览器不可直接识别,需要解析工具解析才可识别

2.2 JSX 编写 JS 代码

在 jsx 中可通过大括号 {} 识别 js 表达式,比如常见的变量、函数调用、方法调用等等

App.js:

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

function getName() {
    return 'jack';
}

const count = 100;
function App() {
  return (
    <div className="App">
      this is React App
        {/*1. 引号传递字符串*/}
        {'this is message'}
        {/*2. 识别 js 变量*/}
        {count}
        {/*3. 函数调用*/}
        { getName() }
        {/*4. 方法调用*/}
        {new Date().getDate()}
        {/*5. 使用 js 对象*/}
        <div style={{ color: 'red' }}>this is div</div>
    </div>
  );
}

export default App;

2.3 JSX 中实现列表渲染

提示:在 JSX 中可以使用原生 JS 中 map 方法遍历渲染列表

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

const count = 100;

const list = [
    {id: 1001, name: 'Vue'},
    {id: 1002, name: 'React'},
    {id: 1003, name: 'Angular'},
];

function App() {
  return (
    <div className="App">
        this is App
        {/*渲染列表*/}
        {list.map(item => <li key={item.id}>{item.name}</li>)}
    </div>
  );
}

export default App;

注意:

  1. 渲染哪个结构就 return 那个

  2. 循环渲染记得要加上独一无二的 key(类型为 string 或 number)

2.4 JSX 实现条件渲染

在 React 中,可以通过逻辑与运算符 &&、三元表达式(?:)实现*基础的条件渲染

类似 Vue 的 v-if

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

const isLogin = true;

function App() {
  return (
    <div className="App">
        {/*1. 逻辑与 &&*/}
        {isLogin && <span>this is span</span>}
        <br/>
        {/*2. 三元运算*/}
        {isLogin ? <span>is Login</span> : <span>not Login</span>}
    </div>
  );
}

export default App;

image-20240706153914830

2.4.1 JSX 条件渲染的 demo
// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

const articleType = 3; // 0 1 3 'articleType' 的取值范围

// 定义核心函数(根据文章类型返回不同的 JSX 模板)
function getArticleTemplate() {
    if (articleType === 0) {
        return <div>我是图文文章</div>
    } else if (articleType === 1) {
        return <div>我是单图文章</div>
    } else {
        return <div>我是三图文章</div>
    }

}

function App() {
  return (
    <div className="App">
        {/*调用函数渲染不同模板*/}
        {getArticleTemplate()}
    </div>
  );
}

export default App;

显示:我是三图文章

3. React 基础事件绑定

语法:on + 事件名 = {事件处理程序/函数名},遵循驼峰命令

1.绑定事件

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

function App() {
    const handleClick = () => {
        console.log('button 被点击了');
    }

  return (
    <div className="App">
        <button onClick={handleClick}>click me</button>
    </div>
  );
}

export default App;

2.传递事件参数 e

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

function App() {

    // 拿到事件参数 e
    const handleClick = (e) => {
        console.log('button 被点击了', e);
    }

  return (
    <div className="App">
        <button onClick={handleClick}>click me</button>
    </div>
  );
}

export default App;

3.传递自定义参数

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

function App() {
	
    // 传递自定义参数
    const handleClick = (name) => {
        console.log('button 被点击了', name);
    }

  return (
    <div className="App">
        {/*箭头函数传参*/}
        <button onClick={() => handleClick('jack')}>click me</button>
    </div>
  );
}

export default App;

4.同时传递自定义参数和事件参数 e

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上



function App() {
    // const handleClick = () => {
    //     console.log('button 被点击了');
    // }

    // 拿到事件参数 e
    // const handleClick = (e) => {
    //     console.log('button 被点击了', e);
    // }

    const handleClick = (name, e) => {
        console.log('button 被点击了', name, e);
    }

  return (
    <div className="App">
        {/*箭头函数传参*/}
        <button onClick={(e) => handleClick('jack', e)}>click me</button>
    </div>
  );
}

export default App;

image-20240706160509512

4. React组件

在 React 中,一个组件就是一个首字母大写的函数,内部含有组件的逻辑和 UI,渲染组件只需将组件当做标签书写即可

1.定义组件(function 定义或者箭头函数)

App.js:

function Button() {
    // 组件逻辑
    return <button>click me</button>
}

const Button = () => {
    // 组件逻辑
    return <button>click me</button>
}

2.渲染组件(自闭和或成对标签)

function App() {
  return (
    <div className="App">
        {/*自闭和*/}
        <Button />
        {/*成对标签*/}
        <Button></Button>
    </div>
  );
}

5. useState 基础使用

其是 React 的一个 Hook,允许我们向组件添加一个状态变量,从而控制影响组件的渲染结果

const [count, setCount] = useState(0);

count 的值不可直接修改,只能通过 setCount 修改

本质:状态变量一旦发生变化组件的视图 UI 也会变化(数据驱动视图

特点:

  1. useState 是一个函数,返回值是一个数组

  2. 数组的第一个参数是状态变量,第二个参数是 set 函数用来修改状态变量

  3. useState 的参数将作为 count 的初始值

一个 useState 的小 demo

App.js:

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

// useState 实现一个计数器按钮
import {useState} from "react";

function App() {
    // 1. 调用 useState 添加一个状态变量
    // count 状态变量
    // setCount 修改状态变量
    const [count, setCount] = useState(0);

    // 2. 点击按钮的回调
    const handleClick = () => {
        // 作用:1.用传入的新值修改 count
        // 2.重新使用新的 count 渲染 UI
        setCount(count + 1);
    };
  return (
    <div className="App">
        <button onClick={handleClick}>{count}</button>
    </div>
  );
}

export default App;

拓展 demo

App.js:

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

// useState 实现一个计数器按钮
import {useState} from "react";

function App() {

    // 修改对象
    const [form, setForm] = useState({name: 'jack'});

    const changeForm = () => {
        // 错误写法:直接修改
        // form.name = 'john';
        // 正确写法:setForm 传入一个全新的对象
        setForm({
            ...form,
            name: 'john',
        })
    };

  return (
    <div className="App">
        <button onClick={changeForm}>修改 form {form.name}</button>
    </div>
  );
}

export default App;

6. 如何修改组件样式

image-20240706212148864

App.js:

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

// 导入样式
import './index.css'
const style = {
    color: 'red',
    fontSize: '50px',
};

function App() {

  return (
    <div className="App">
        {/*行内控制*/}
        <span style={{color: 'red', fontSize: '50px'}}>this is span</span>
        <span style={style}>this is span</span>
        {/*通过 class 类名控制*/}
        <span className='foo'>this is foo</span>
    </div>
  );
}

export default App;

index.css:

.foo {
    color: blue;
}

7. B 站评论案例

7.1 列表渲染

App.js:

import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";

/**
 * 评论列表的渲染和操作
 *
 * 1. 根据状态渲染评论列表
 * 2. 删除评论
 */

// 评论列表数据
const list = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-18 08:15',
    like: 88,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '11-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '黑马前端',
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
  },
]


// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar,
  // 用户昵称
  uname: '黑马前端',
}

/**
 * 导航 Tab 的渲染和操作
 *
 * 1. 渲染导航 Tab 和高亮
 * 2. 评论列表排序
 *  最热 => 喜欢数量降序
 *  最新 => 创建时间降序
 */

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

// 渲染评论列表
// 1.使用 useState 维护 list


const App = () => {

  const [commentList, setCommentList] = useState(list);

  return (
    <div className="app">
      {/* 导航 Tab */}
      <div className="reply-navigation">
        <ul className="nav-bar">
          <li className="nav-title">
            <span className="nav-title-text">评论</span>
            {/* 评论数量 */}
            <span className="total-reply">{10}</span>
          </li>
          <li className="nav-sort">
            {/* 高亮类名: active */}
            <span className='nav-item'>最新</span>
            <span className='nav-item'>最热</span>
          </li>
        </ul>
      </div>

      <div className="reply-wrap">
        {/* 发表评论 */}
        <div className="box-normal">
          {/* 当前用户头像 */}
          <div className="reply-box-avatar">
            <div className="bili-avatar">
              <img className="bili-avatar-img" src={avatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <div className="send-text">发布</div>
            </div>
          </div>
        </div>
        {/* 评论列表 */}
        <div className="reply-list">
          {/* 评论项 */}
          {commentList.map(item => (
              <div key={item.rpid} className="reply-item">
                {/* 头像 */}
                <div className="root-reply-avatar">
                  <div className="bili-avatar">
                    <img
                        className="bili-avatar-img"
                        alt=""
                        src={item.user.avatar}
                    />
                  </div>
                </div>

                <div className="content-wrap">
                  {/* 用户名 */}
                  <div className="user-info">
                    <div className="user-name">{item.user.uname}</div>
                  </div>
                  {/* 评论内容 */}
                  <div className="root-reply">
                    <span className="reply-content">{item.content}</span>
                    <div className="reply-info">
                      {/* 评论时间 */}
                      <span className="reply-time">{item.ctime}</span>
                      {/* 评论数量 */}
                      <span className="reply-time">点赞数:{item.like}</span>
                      <span className="delete-btn">
                    删除
                  </span>

                    </div>
                  </div>
                </div>
              </div>
          ))}

        </div>
      </div>
    </div>
  )
}

export default App

App.scss:

.app {
  width: 80%;
  margin: 50px auto;
}

.reply-navigation {
  margin-bottom: 22px;

  .nav-bar {
    display: flex;
    align-items: center;
    margin: 0;
    padding: 0;
    list-style: none;

    .nav-title {
      display: flex;
      align-items: center;
      width: 114px;
      font-size: 20px;

      .nav-title-text {
        color: #18191c;
        font-weight: 500;
      }
      .total-reply {
        margin: 0 36px 0 6px;
        color: #9499a0;
        font-weight: normal;
        font-size: 13px;
      }
    }

    .nav-sort {
      display: flex;
      align-items: center;
      color: #9499a0;
      font-size: 13px;

      .nav-item {
        cursor: pointer;

        &:hover {
          color: #00aeec;
        }

        &:last-child::after {
          display: none;
        }
        &::after {
          content: ' ';
          display: inline-block;
          height: 10px;
          width: 1px;
          margin: -1px 12px;
          background-color: #9499a0;
        }
      }

      .nav-item.active {
        color: #18191c;
      }
    }
  }
}

.reply-wrap {
  position: relative;
}
.box-normal {
  display: flex;
  transition: 0.2s;

  .reply-box-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 80px;
    height: 50px;
  }

  .reply-box-wrap {
    display: flex;
    position: relative;
    flex: 1;

    .reply-box-textarea {
      width: 100%;
      height: 50px;
      padding: 5px 10px;
      box-sizing: border-box;
      color: #181931;
      font-family: inherit;
      line-height: 38px;
      background-color: #f1f2f3;
      border: 1px solid #f1f2f3;
      border-radius: 6px;
      outline: none;
      resize: none;
      transition: 0.2s;

      &::placeholder {
        color: #9499a0;
        font-size: 12px;
      }
      &:focus {
        height: 60px;
        background-color: #fff;
        border-color: #c9ccd0;
      }
    }
  }

  .reply-box-send {
    position: relative;
    display: flex;
    flex-basis: 86px;
    align-items: center;
    justify-content: center;
    margin-left: 10px;
    border-radius: 4px;
    cursor: pointer;
    transition: 0.2s;

    & .send-text {
      position: absolute;
      z-index: 1;
      color: #fff;
      font-size: 16px;
    }
    &::after {
      position: absolute;
      width: 100%;
      height: 100%;
      background-color: #00aeec;
      border-radius: 4px;
      opacity: 0.5;
      content: '';
    }
    &:hover::after {
      opacity: 1;
    }
  }
}
.bili-avatar {
  position: relative;
  display: block;
  width: 48px;
  height: 48px;
  margin: 0;
  padding: 0;
  border-radius: 50%;
}
.bili-avatar-img {
  position: absolute;
  top: 50%;
  left: 50%;
  display: block;
  width: 48px;
  height: 48px;
  object-fit: cover;
  border: none;
  border-radius: 50%;
  image-rendering: -webkit-optimize-contrast;
  transform: translate(-50%, -50%);
}

// 评论列表
.reply-list {
  margin-top: 14px;
}
.reply-item {
  padding: 22px 0 0 80px;
  .root-reply-avatar {
    position: absolute;
    left: 0;
    display: flex;
    justify-content: center;
    width: 80px;
    cursor: pointer;
  }

  .content-wrap {
    position: relative;
    flex: 1;

    &::after {
      content: ' ';
      display: block;
      height: 1px;
      width: 100%;
      margin-top: 14px;
      background-color: #e3e5e7;
    }

    .user-info {
      display: flex;
      align-items: center;
      margin-bottom: 4px;

      .user-name {
        height: 30px;
        margin-right: 5px;
        color: #61666d;
        font-size: 13px;
        line-height: 30px;
        cursor: pointer;
      }
    }

    .root-reply {
      position: relative;
      padding: 2px 0;
      color: #181931;
      font-size: 15px;
      line-height: 24px;
      .reply-info {
        position: relative;
        display: flex;
        align-items: center;
        margin-top: 2px;
        color: #9499a0;
        font-size: 13px;

        .reply-time {
          width: 86px;
          margin-right: 20px;
        }
        .reply-like {
          display: flex;
          align-items: center;
          margin-right: 19px;

          .like-icon {
            width: 14px;
            height: 14px;
            margin-right: 5px;
            color: #9499a0;
            background-position: -153px -25px;
            &:hover {
              background-position: -218px -25px;
            }
          }
          .like-icon.liked {
            background-position: -154px -89px;
          }
        }
        .reply-dislike {
          display: flex;
          align-items: center;
          margin-right: 19px;
          .dislike-icon {
            width: 16px;
            height: 16px;
            background-position: -153px -153px;
            &:hover {
              background-position: -217px -153px;
            }
          }
          .dislike-icon.disliked {
            background-position: -154px -217px;
          }
        }
        .delete-btn {
          cursor: pointer;
          &:hover {
            color: #00aeec;
          }
        }
      }
    }
  }
}

.reply-none {
  height: 64px;
  margin-bottom: 80px;
  color: #99a2aa;
  font-size: 13px;
  line-height: 64px;
  text-align: center;
}

image-20240706224956160

7.2 删除功能实现

需求:

  1. 只有自己的评论才可以删除

  2. 点击删除按钮,删除当前评论,列表中不再显示

核心思路:

  1. 删除显示 - 条件渲染

  2. 删除功能 - 拿到当前项 id 以 id 为条件对评论列表做过滤

7.3 渲染 Tab + 点击高亮实现

需求:点击哪个 tab 项,哪个做高亮处理

核心思路:

点击谁就把谁的type(独一无二的标识)记录下来,然后和遍历时的每一项的type做匹配,谁匹配到就设置负责高亮的类名

import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";

/**
 * 评论列表的渲染和操作
 *
 * 1. 根据状态渲染评论列表
 * 2. 删除评论
 */

// 评论列表数据
const list = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-18 08:15',
    like: 88,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '11-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '黑马前端',
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
  },
]


// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar,
  // 用户昵称
  uname: '黑马前端',
}

/**
 * 导航 Tab 的渲染和操作
 *
 * 1. 渲染导航 Tab 和高亮
 * 2. 评论列表排序
 *  最热 => 喜欢数量降序
 *  最新 => 创建时间降序
 */

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

const App = () => {

  // 渲染评论列表
  // 1.使用 useState 维护 list
  const [commentList, setCommentList] = useState(list);

  // 删除功能
  const handleDel = (id) => {
    // 对 commentList 进行过滤
    setCommentList(commentList.filter(item => item.rpid !== id));
  };

  // tab 切换功能
  // 1.点击谁就把谁的 type 记录下来
  // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
  const [type, setType] = useState('hot');
  const handleTabChange = (type) => {
    console.log(type);
    setType(type);
  };

  return (
    <div className="app">
      {/* 导航 Tab */}
      <div className="reply-navigation">
        <ul className="nav-bar">
          <li className="nav-title">
            <span className="nav-title-text">评论</span>
            {/* 评论数量 */}
            <span className="total-reply">{10}</span>
          </li>
          <li className="nav-sort">
            {/* 高亮类名: active */}
            {tabs.map(item =>
                <span key={item.type}
                      onClick={() => handleTabChange(item.type)}
                      className={`nav-item ${type === item.type && 'active'}`}>
                  {item.text}
                </span>)}
          </li>
        </ul>
      </div>

      <div className="reply-wrap">
        {/* 发表评论 */}
        <div className="box-normal">
          {/* 当前用户头像 */}
          <div className="reply-box-avatar">
            <div className="bili-avatar">
              <img className="bili-avatar-img" src={avatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <div className="send-text">发布</div>
            </div>
          </div>
        </div>
        {/* 评论列表 */}
        <div className="reply-list">
          {/* 评论项 */}
          {commentList.map(item => (
              <div key={item.rpid} className="reply-item">
                {/* 头像 */}
                <div className="root-reply-avatar">
                  <div className="bili-avatar">
                    <img
                        className="bili-avatar-img"
                        alt=""
                        src={item.user.avatar}
                    />
                  </div>
                </div>

                <div className="content-wrap">
                  {/* 用户名 */}
                  <div className="user-info">
                    <div className="user-name">{item.user.uname}</div>
                  </div>
                  {/* 评论内容 */}
                  <div className="root-reply">
                    <span className="reply-content">{item.content}</span>
                    <div className="reply-info">
                      {/* 评论时间 */}
                      <span className="reply-time">{item.ctime}</span>
                      {/* 评论数量 */}
                      <span className="reply-time">点赞数:{item.like}</span>
                      {/*条件:user.id === item.user.id*/}
                      {user.uid === item.user.uid  && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}

                    </div>
                  </div>
                </div>
              </div>
          ))}

        </div>
      </div>
    </div>
  )
}

export default App

7.4 排序功能

需求:点击最新,评论列表按照创建时间倒序排列(新的在前),点击最热按照点赞数排序(多的在前)

核心思路:把评论列表状态数据进行不同的排序处理,当成新值传给 set 函数重新渲染视图 UI

lodash 库

安装:

npm install lodash

引入:

import _ from 'lodash';

使用:

setCommentList(_.orderBy(commentList, 'like', 'desc'));

App.js:

import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";
import _ from 'lodash';
/**
 * 评论列表的渲染和操作
 *
 * 1. 根据状态渲染评论列表
 * 2. 删除评论
 */

// 评论列表数据
const list = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-20 08:15',
    like: 38,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '09-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '黑马前端',
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
  },
]


// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar,
  // 用户昵称
  uname: '黑马前端',
}

/**
 * 导航 Tab 的渲染和操作
 *
 * 1. 渲染导航 Tab 和高亮
 * 2. 评论列表排序
 *  最热 => 喜欢数量降序
 *  最新 => 创建时间降序
 */

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

const App = () => {

  // 渲染评论列表
  // 1.使用 useState 维护 list
  const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));

  // 删除功能
  const handleDel = (id) => {
    // 对 commentList 进行过滤
    setCommentList(commentList.filter(item => item.rpid !== id));
  };

  // tab 切换功能
  // 1.点击谁就把谁的 type 记录下来
  // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
  const [type, setType] = useState('hot');
  const handleTabChange = (type) => {
    console.log(type);
    setType(type);
    // 基于列表的排序
    if (type === 'hot') {
      // 根据点赞数量排序
      // lodash
      setCommentList(_.orderBy(commentList, 'like', 'desc'));
    } else {
      // 根据创建时间排序
      setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
    }
  };

  return (
    <div className="app">
      {/* 导航 Tab */}
      <div className="reply-navigation">
        <ul className="nav-bar">
          <li className="nav-title">
            <span className="nav-title-text">评论</span>
            {/* 评论数量 */}
            <span className="total-reply">{10}</span>
          </li>
          <li className="nav-sort">
            {/* 高亮类名: active */}
            {tabs.map(item =>
                <span key={item.type}
                      onClick={() => handleTabChange(item.type)}
                      className={`nav-item ${type === item.type && 'active'}`}>
                  {item.text}
                </span>)}
          </li>
        </ul>
      </div>

      <div className="reply-wrap">
        {/* 发表评论 */}
        <div className="box-normal">
          {/* 当前用户头像 */}
          <div className="reply-box-avatar">
            <div className="bili-avatar">
              <img className="bili-avatar-img" src={avatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <div className="send-text">发布</div>
            </div>
          </div>
        </div>
        {/* 评论列表 */}
        <div className="reply-list">
          {/* 评论项 */}
          {commentList.map(item => (
              <div key={item.rpid} className="reply-item">
                {/* 头像 */}
                <div className="root-reply-avatar">
                  <div className="bili-avatar">
                    <img
                        className="bili-avatar-img"
                        alt=""
                        src={item.user.avatar}
                    />
                  </div>
                </div>

                <div className="content-wrap">
                  {/* 用户名 */}
                  <div className="user-info">
                    <div className="user-name">{item.user.uname}</div>
                  </div>
                  {/* 评论内容 */}
                  <div className="root-reply">
                    <span className="reply-content">{item.content}</span>
                    <div className="reply-info">
                      {/* 评论时间 */}
                      <span className="reply-time">{item.ctime}</span>
                      {/* 评论数量 */}
                      <span className="reply-time">点赞数:{item.like}</span>
                      {/*条件:user.id === item.user.id*/}
                      {user.uid === item.user.uid  && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}

                    </div>
                  </div>
                </div>
              </div>
          ))}

        </div>
      </div>
    </div>
  )
}

export default App

8. classnams 优化类名控制

classnams 是一个简单的 JS 库,可以非常方便的通过条件动态控制 class 类名的显示

以前出现的问题:

image-20240707135835054

语法:key 表示要控制的类名,value 表示条件,true 的时候类名就会显示

使用

安装:

npm install classnames

引入:

import classNames from "classnames";

用法:

    <span key={item.type}
      	onClick={() => handleTabChange(item.type)}
      	className={classNames('nav-item', {active: type === item.type})}>
      	{item.text}
    </span>)}

9. 受表单控制项

概念:使用 React 组件的状态(useState)控制表单状态

image-20240707141640229

App.js

// 项目的根组件
// App -> index.js -> public/index.html(root)  => App 根组件被导入到 index.js,然后渲染到 index.html 的 root 节点上

// 1.声明一个 react 状态 - useState
// 2.核心绑定流程
// 2.1通过 value 属性绑定 react 状态
// 2.2绑定 onChange 事件,通过事件参数 e 拿到输入框最新值,反向修改 react 状态

import {useState} from "react";

function App() {

   const [value, setValue] =  useState();

  return (
    <div className="App">
        <input
            value={value}
            onChange={(e) => setValue(e.target.value)}
            type='text'
        />
    </div>
  );
}

export default App;

10. React 中获取DOM

在 React 中获取/操作 DOM,需要使用 useRef 钩子函数,分为两步:

  1. 使用 useRef 创建 ref 对象,并与 JSX 绑定

  2. 在 DOM 可用/DOM 渲染完毕时,通过 inputRef.current 拿到 DOM 对象

App.js:

import React, { useRef } from "react";

function App() {
    const inputRef = useRef(null);

    const showDom = () => {
        console.log(inputRef.current);
    };

    const setInputValue = () => {
        inputRef.current.value = '新的值';
    };

    const focusInput = () => {
        inputRef.current.focus();
    };

    const selectInputText = () => {
        inputRef.current.select();
    };

    const addClassToInput = () => {
        inputRef.current.classList.add('new-class');
    };

    const removeClassFromInput = () => {
        inputRef.current.classList.remove('new-class');
    };

    const addEventListenerToInput = () => {
        inputRef.current.addEventListener('input', (event) => {
            console.log('Input changed:', event.target.value);
        });
    };

    return (
        <div className="App">
            <input ref={inputRef} type='text'/>
            <button onClick={showDom}>获取 dom</button>
            <button onClick={setInputValue}>设置值</button>
            <button onClick={focusInput}>聚焦</button>
            <button onClick={selectInputText}>选择文本</button>
            <button onClick={addClassToInput}>添加类</button>
            <button onClick={removeClassFromInput}>移除类</button>
            <button onClick={addEventListenerToInput}>添加事件监听器</button>
        </div>
    );
}

export default App;

11.B 站评论优化

11.1 发表评论

App.scss:

.app {
  width: 80%;
  margin: 50px auto;
}

.reply-navigation {
  margin-bottom: 22px;

  .nav-bar {
    display: flex;
    align-items: center;
    margin: 0;
    padding: 0;
    list-style: none;

    .nav-title {
      display: flex;
      align-items: center;
      width: 114px;
      font-size: 20px;

      .nav-title-text {
        color: #18191c;
        font-weight: 500;
      }
      .total-reply {
        margin: 0 36px 0 6px;
        color: #9499a0;
        font-weight: normal;
        font-size: 13px;
      }
    }

    .nav-sort {
      display: flex;
      align-items: center;
      color: #9499a0;
      font-size: 13px;

      .nav-item {
        cursor: pointer;

        &:hover {
          color: #00aeec;
        }

        &:last-child::after {
          display: none;
        }
        &::after {
          content: ' ';
          display: inline-block;
          height: 10px;
          width: 1px;
          margin: -1px 12px;
          background-color: #9499a0;
        }
      }

      .nav-item.active {
        color: #18191c;
      }
    }
  }
}

.reply-wrap {
  position: relative;
}
.box-normal {
  display: flex;
  transition: 0.2s;

  .reply-box-avatar {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 80px;
    height: 50px;
  }

  .reply-box-wrap {
    display: flex;
    position: relative;
    flex: 1;

    .reply-box-textarea {
      width: 100%;
      height: 50px;
      padding: 5px 10px;
      box-sizing: border-box;
      color: #181931;
      font-family: inherit;
      line-height: 38px;
      background-color: #f1f2f3;
      border: 1px solid #f1f2f3;
      border-radius: 6px;
      outline: none;
      resize: none;
      transition: 0.2s;

      &::placeholder {
        color: #9499a0;
        font-size: 12px;
      }
      &:focus {
        height: 60px;
        background-color: #fff;
        border-color: #c9ccd0;
      }
    }
  }

  .reply-box-send {
    position: relative;
    display: flex;
    flex-basis: 86px;
    align-items: center;
    justify-content: center;
    margin-left: 10px;
    border-radius: 4px;
    cursor: pointer;
    transition: 0.2s;

    & .send-text {
      position: absolute;
      z-index: 1;
      color: #fff;
      font-size: 16px;
    }
    &::after {
      position: absolute;
      width: 100%;
      height: 100%;
      background-color: #00aeec;
      border-radius: 4px;
      opacity: 0.5;
      content: '';
    }
    &:hover::after {
      opacity: 1;
    }
  }
}
.bili-avatar {
  position: relative;
  display: block;
  width: 48px;
  height: 48px;
  margin: 0;
  padding: 0;
  border-radius: 50%;
}
.bili-avatar-img {
  position: absolute;
  top: 50%;
  left: 50%;
  display: block;
  width: 48px;
  height: 48px;
  object-fit: cover;
  border: none;
  border-radius: 50%;
  image-rendering: -webkit-optimize-contrast;
  transform: translate(-50%, -50%);
}

// 评论列表
.reply-list {
  margin-top: 14px;
}
.reply-item {
  padding: 22px 0 0 80px;
  .root-reply-avatar {
    position: absolute;
    left: 0;
    display: flex;
    justify-content: center;
    width: 80px;
    cursor: pointer;
  }

  .content-wrap {
    position: relative;
    flex: 1;

    &::after {
      content: ' ';
      display: block;
      height: 1px;
      width: 100%;
      margin-top: 14px;
      background-color: #e3e5e7;
    }

    .user-info {
      display: flex;
      align-items: center;
      margin-bottom: 4px;

      .user-name {
        height: 30px;
        margin-right: 5px;
        color: #61666d;
        font-size: 13px;
        line-height: 30px;
        cursor: pointer;
      }
    }

    .root-reply {
      position: relative;
      padding: 2px 0;
      color: #181931;
      font-size: 15px;
      line-height: 24px;
      .reply-info {
        position: relative;
        display: flex;
        align-items: center;
        margin-top: 2px;
        color: #9499a0;
        font-size: 13px;

        .reply-time {
          width: 86px;
          margin-right: 20px;
        }
        .reply-like {
          display: flex;
          align-items: center;
          margin-right: 19px;

          .like-icon {
            width: 14px;
            height: 14px;
            margin-right: 5px;
            color: #9499a0;
            background-position: -153px -25px;
            &:hover {
              background-position: -218px -25px;
            }
          }
          .like-icon.liked {
            background-position: -154px -89px;
          }
        }
        .reply-dislike {
          display: flex;
          align-items: center;
          margin-right: 19px;
          .dislike-icon {
            width: 16px;
            height: 16px;
            background-position: -153px -153px;
            &:hover {
              background-position: -217px -153px;
            }
          }
          .dislike-icon.disliked {
            background-position: -154px -217px;
          }
        }
        .delete-btn {
          cursor: pointer;
          &:hover {
            color: #00aeec;
          }
        }
      }
    }
  }
}

.reply-none {
  height: 64px;
  margin-bottom: 80px;
  color: #99a2aa;
  font-size: 13px;
  line-height: 64px;
  text-align: center;
}

App.js:

import './App.scss'
import avatar from './images/bozai.png'
import {useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
/**
 * 评论列表的渲染和操作
 *
 * 1. 根据状态渲染评论列表
 * 2. 删除评论
 */

// 评论列表数据
const list = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-20 08:15',
    like: 38,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '09-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '黑马前端',
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
  },
]


// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar,
  // 用户昵称
  uname: '黑马前端',
}

/**
 * 导航 Tab 的渲染和操作
 *
 * 1. 渲染导航 Tab 和高亮
 * 2. 评论列表排序
 *  最热 => 喜欢数量降序
 *  最新 => 创建时间降序
 */

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

const App = () => {

  // 渲染评论列表
  // 1.使用 useState 维护 list
  const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));

  // 删除功能
  const handleDel = (id) => {
    // 对 commentList 进行过滤
    setCommentList(commentList.filter(item => item.rpid !== id));
  };

  // tab 切换功能
  // 1.点击谁就把谁的 type 记录下来
  // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
  const [type, setType] = useState('hot');
  const handleTabChange = (type) => {
    console.log(type);
    setType(type);
    // 基于列表的排序
    if (type === 'hot') {
      // 根据点赞数量排序
      // lodash
      setCommentList(_.orderBy(commentList, 'like', 'desc'));
    } else {
      // 根据创建时间排序
      setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
    }
  };

  // 发表评论
  const [content, setContent] = useState('');
  const handlePublish = () => {
    setCommentList([
      ...commentList,
      {
        rpid: 4,
        user: {
          uid: '30009257',
          avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
          uname: '黑马前端',
        },
        content: content,
        ctime: '10-19 09:00',
        like: 80,
      }
      ]);
  };

  return (
    <div className="app">
      {/* 导航 Tab */}
      <div className="reply-navigation">
        <ul className="nav-bar">
          <li className="nav-title">
            <span className="nav-title-text">评论</span>
            {/* 评论数量 */}
            <span className="total-reply">{10}</span>
          </li>
          <li className="nav-sort">
            {/* 高亮类名: active */}
            {tabs.map(item =>
                <span key={item.type}
                      onClick={() => handleTabChange(item.type)}
                      className={classNames('nav-item', {active: type === item.type})}>
                  {item.text}
                </span>)
            }
          </li>
        </ul>
      </div>

      <div className="reply-wrap">
        {/* 发表评论 */}
        <div className="box-normal">
          {/* 当前用户头像 */}
          <div className="reply-box-avatar">
            <div className="bili-avatar">
              <img className="bili-avatar-img" src={avatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
              value={content}
              onChange={(e) => setContent(e.target.value)}
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <div className="send-text" onClick={handlePublish}>发布</div>
            </div>
          </div>
        </div>
        {/* 评论列表 */}
        <div className="reply-list">
          {/* 评论项 */}
          {commentList.map(item => (
              <div key={item.rpid} className="reply-item">
                {/* 头像 */}
                <div className="root-reply-avatar">
                  <div className="bili-avatar">
                    <img
                        className="bili-avatar-img"
                        alt=""
                        src={item.user.avatar}
                    />
                  </div>
                </div>

                <div className="content-wrap">
                  {/* 用户名 */}
                  <div className="user-info">
                    <div className="user-name">{item.user.uname}</div>
                  </div>
                  {/* 评论内容 */}
                  <div className="root-reply">
                    <span className="reply-content">{item.content}</span>
                    <div className="reply-info">
                      {/* 评论时间 */}
                      <span className="reply-time">{item.ctime}</span>
                      {/* 评论数量 */}
                      <span className="reply-time">点赞数:{item.like}</span>
                      {/*条件:user.id === item.user.id*/}
                      {user.uid === item.user.uid  && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
                    </div>
                  </div>
                </div>
              </div>
          ))}

        </div>
      </div>
    </div>
  )
}

export default App

image-20240707150807010

uuid 库

安装:

npm install uuid

引入:

import {v4 as uuidV4} from 'uuid'
uuidV4(); // 使用

dayjs 库

安装:

npm install dayjs

引入:

import dayjs from 'dayjs'
dayjs() // 使用

11.2 发表评论后清除输入框并聚焦

思路:

  1. 设置输入框的 useState 的 setContent 为空

  2. 利用 useRef 获取 dom 元素,再调用 focus 方法

App.js:

import './App.scss'
import avatar from './images/bozai.png'
import {useRef, useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
import {v4 as uuidV4} from 'uuid'
import dayjs from "dayjs";

/**
 * 评论列表的渲染和操作
 *
 * 1. 根据状态渲染评论列表
 * 2. 删除评论
 */

// 评论列表数据
const list = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-20 08:15',
    like: 38,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '09-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '黑马前端',
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
  },
]


// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar,
  // 用户昵称
  uname: '黑马前端',
}

/**
 * 导航 Tab 的渲染和操作
 *
 * 1. 渲染导航 Tab 和高亮
 * 2. 评论列表排序
 *  最热 => 喜欢数量降序
 *  最新 => 创建时间降序
 */

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

const App = () => {

  // 渲染评论列表
  // 1.使用 useState 维护 list
  const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));

  const inputRef = useRef(null);

  // 删除功能
  const handleDel = (id) => {
    // 对 commentList 进行过滤
    setCommentList(commentList.filter(item => item.rpid !== id));
  };

  // tab 切换功能
  // 1.点击谁就把谁的 type 记录下来
  // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
  const [type, setType] = useState('hot');
  const handleTabChange = (type) => {
    console.log(type);
    setType(type);
    // 基于列表的排序
    if (type === 'hot') {
      // 根据点赞数量排序
      // lodash
      setCommentList(_.orderBy(commentList, 'like', 'desc'));
    } else {
      // 根据创建时间排序
      setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
    }
  };

  // 发表评论
  const [content, setContent] = useState('');
  const handlePublish = () => {
    setCommentList([
      ...commentList,
      {
        rpid: uuidV4(),
        user: {
          uid: '30009257',
          avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
          uname: '黑马前端',
        },
        content: content,
        ctime: dayjs(new Date()).format('MM-DD hh:mm'),
        like: 80,
      }
      ]);
    // 1.清楚输入框内容
    setContent('')
    // 2.重新聚焦
    inputRef.current.focus();
  };

  return (
    <div className="app">
      {/* 导航 Tab */}
      <div className="reply-navigation">
        <ul className="nav-bar">
          <li className="nav-title">
            <span className="nav-title-text">评论</span>
            {/* 评论数量 */}
            <span className="total-reply">{10}</span>
          </li>
          <li className="nav-sort">
            {/* 高亮类名: active */}
            {tabs.map(item =>
                <span key={item.type}
                      onClick={() => handleTabChange(item.type)}
                      className={classNames('nav-item', {active: type === item.type})}>
                  {item.text}
                </span>)
            }
          </li>
        </ul>
      </div>

      <div className="reply-wrap">
        {/* 发表评论 */}
        <div className="box-normal">
          {/* 当前用户头像 */}
          <div className="reply-box-avatar">
            <div className="bili-avatar">
              <img className="bili-avatar-img" src={avatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
              value={content}
              onChange={(e) => setContent(e.target.value)}
              ref={inputRef}
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <div className="send-text" onClick={handlePublish}>发布</div>
            </div>
          </div>
        </div>
        {/* 评论列表 */}
        <div className="reply-list">
          {/* 评论项 */}
          {commentList.map(item => (
              <div key={item.rpid} className="reply-item">
                {/* 头像 */}
                <div className="root-reply-avatar">
                  <div className="bili-avatar">
                    <img
                        className="bili-avatar-img"
                        alt=""
                        src={item.user.avatar}
                    />
                  </div>
                </div>

                <div className="content-wrap">
                  {/* 用户名 */}
                  <div className="user-info">
                    <div className="user-name">{item.user.uname}</div>
                  </div>
                  {/* 评论内容 */}
                  <div className="root-reply">
                    <span className="reply-content">{item.content}</span>
                    <div className="reply-info">
                      {/* 评论时间 */}
                      <span className="reply-time">{item.ctime}</span>
                      {/* 评论数量 */}
                      <span className="reply-time">点赞数:{item.like}</span>
                      {/*条件:user.id === item.user.id*/}
                      {user.uid === item.user.uid  && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
                    </div>
                  </div>
                </div>
              </div>
          ))}

        </div>
      </div>
    </div>
  )
}

export default App

12. 组件间通信

父传子 props

传递步骤

1.父组件传递数据,子组件标签身上绑定属性 2.子组件接收数据,props 参数

父传子 demo:

App.js:

import React from "react";

// 父传子
// 1.父组件传递数据,子组件标签身上绑定属性
// 2.子组件接收数据,props 参数
function Son (props) {
    // props:对象包含了父组件传递过来的所有数据
    console.log(props);
    return <div>this is son, father's param is {props.name}</div>;
}

function App() {

    const name = 'this is app name';

    return (
        <div className="App">
            <Son name={name} />
        </div>
    );
}

export default App;

小 demo:

App.js:

import React from "react";

// 父传子
// 1.父组件传递数据,子组件标签身上绑定属性
// 2.子组件接收数据,props 参数
function Son (props) {
    // props:对象包含了父组件传递过来的所有数据
    console.log(props);
    return <div>this is son, father's param is {props.name}</div>;
}

function App() {

    const name = 'this is app name';

    return (
        <div className="App">
            <Son
                name={name}
                age={18}
                isTrue={false}
                list={['vue', 'react']}
                cb={() => console.log(123)}
                child={<span>this is span</span>}
            />
        </div>
    );
}

export default App;

注意

  1. 父组件几乎可以给子组件传任何东西,包括布尔,数值,数组,对象和函数等

  2. 但是子组件不可修改父组件传递的属性,谁传递的谁修改

父传子 特殊的 prop children

组件包裹传递

App.js:

import React from "react";

// 父传子
// 1.父组件传递数据,子组件标签身上绑定属性
// 2.子组件接收数据,props 参数
function Son (props) {
    console.log(props)
    return <div>this is son, {props.children}</div>
}

function App() {
    return (
        <div className="App">
            <Son>
                <span>this is span</span>
            </Son>
        </div>
    );
}

export default App;

显示:this is son,this is span

子传父

思路:子组件调用父组件中的函数并传递参数

App.js:

import React, {useState} from "react";

// 核心:在子组件中调用父组件中的函数并传递实参
function Son ({onGetSonMsg}) {
    const sonMsg = 'this is son msg';
    return (
        <div>
            this is son
            <button onClick={() => onGetSonMsg(sonMsg)}>sendMsg</button>
        </div>
    );
}

function App() {
    const [msg, setMsg] = useState('');
    const getMsg = (msg) => {
        console.log(msg);
        setMsg(msg);
    };
    return (
        <div className="App">
            this is App, {msg}
            <Son onGetSonMsg={getMsg} />
        </div>
    );
}

export default App;

使用状态提升实现兄弟组件通信

思路:借助“状态提升”机制,通过父组件进行兄弟组件之间的数据传递

image-20240707205817066

App.js:

import React, {useState} from "react";

// 1.子传父 A -> App
// 2.子传父 B -> App
function A ({onGetAName}) {
    // A 组件中的数据
    const name = 'this is A name';
    return (
        <div>
            this is A component
            <button onClick={() => onGetAName(name)}>send</button>
        </div>
    );
}

function B (props) {
    return (
        <div>
            this is B component, {props.name}
        </div>
    );
}

function App() {

    const [name, setName] = useState('');
    const getAName = (name) => {
        console.log(name);
        setName(name);
    };

    return (
        <div className="App">
            this is App
            <A onGetAName={getAName}/>
            <B name={name}/>
        </div>
    );
}

export default App;

使用 context 机制跨层级组件通信

实现步骤:

  1. 使用createContext方法创建一个上下文对象Ctx

  2. 在顶层组件(App)中通过Ctx.Provider组件提供数据

  3. 在底层组件(B)中通过useContext钩子函数获取消费数据

image-20240707212308649

App.js:

import React, {createContext, useContext} from "react";

// App -> A -> B

// 1.createContext 方法创建一个上下文对象
const MsgContext = createContext();
// 2.在顶层组件通过 Provider 组件提供数据
// 3.在底层组件通过 useContext 钩子函数使用数据

function A () {
    return (
        <div>
            this is A component
            <B />
        </div>
    );
}

function B () {
    const msg = useContext(MsgContext);
    return (
        <div>
            this is B component, {msg}
        </div>
    );
}

function App() {
    const msg = 'this is app msg';
    return (
        <div className="App">
            <MsgContext.Provider value={msg}>
                this is App
                <A />
            </MsgContext.Provider>
        </div>
    );
}

export default App;

结果:

image-20240707213150328

使用场景:

image-20240707213956128

13. UseEffect概念理解

useEffect 用于在 React 组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送 ajax 请求,更改 DOM 等。

语法:

useEffect(() => {}, [])

参数1是一个函数,可以把它叫做副作用函数,在内部放置要执行的操作

参数2是一个数组,在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行。当是空数组时,副作用函数只会在组件渲染完毕后执行一次。

App.js:

import React, {useEffect, useState} from "react";

const URL = 'http://geek.itheima.net/v1_0/channels';

function App() {
    // 创建一个状态数据
    const [list, setList] = useState([]);
    useEffect(() => {
        // 额外的操作 获取频道列表
        async function getList() {
            const res = await fetch(URL);
            const jsonRes = await res.json();
            console.log(jsonRes);
            setList(jsonRes.data.channels);
        }
        getList();
    }, []);

    return (
        <div className="App">
            <ul>
                {list.map(item => <li key={item.id}>{item.name}</li>)}
            </ul>
        </div>
    );
}

export default App;

useEffect 依赖参数说明

image-20240713144310801

情况一:

function App() {
    // 1. 没有依赖项 初始 + 组件更新
    const [count, setCount] = useState(0);
    useEffect(() => {
        console.log('副作用函数执行了');
    });

    return (
        <div className="App">
            this is app
            <button onClick={() => {setCount(count + 1)}}>+{count}</button>
        </div>
    );
}

export default App;

情况二:

function App() {
    // 1. 没有依赖项 初始 + 组件更新
    const [count, setCount] = useState(0);
    // useEffect(() => {
    //     console.log('副作用函数执行了');
    // });

    // 2. 传入空数组依赖 只在初始渲染时执行
    useEffect(() => {
        console.log('副作用函数执行了');
    }, []);

    return (
        <div className="App">
            this is app
            <button onClick={() => {setCount(count + 1)}}>+{count}</button>
        </div>
    );
}

export default App;

情况三:

import React, {useEffect, useState} from "react";

function App() {
    // 1. 没有依赖项 初始 + 组件更新
    const [count, setCount] = useState(0);
    // useEffect(() => {
    //     console.log('副作用函数执行了');
    // });

    // 2. 传入空数组依赖 只在初始渲染时执行
    // useEffect(() => {
    //     console.log('副作用函数执行了');
    // }, []);
    // 3. 传入特定依赖项  初始 + 依赖项变化时执行
    useEffect(() => {
        console.log('副作用函数执行了');
    }, [count]);

    return (
        <div className="App">
            this is app
            <button onClick={() => {setCount(count + 1)}}>+{count}</button>
        </div>
    );
}

export default App;

useEffect 清除副作用

在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开 启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用

import React, {useEffect, useState} from "react";

function Son() {
    useEffect(() => {
        const timer = setInterval(() => {
            console.log('定时器执行中……')
        }, 1000);

        return () => {
            // 清除副作用(组件卸载时)
            clearInterval(timer);
        };
    }, []);

    return <div>this is son</div>
}

function App() {

    const [show, setShow] = useState(true);

    return (
        <div className="App">
            {show && <Son />}
            <button onClick={() => setShow(false)}>卸载 Son 组件</button>
        </div>
    );
}

export default App;

说明:清除副作用的函数最常见的执行时机是组件卸载时自动执行

需求:Son 组件渲染时开启一个定时器,卸载时清除它

14. 自定义 Hook 函数

概念:自定义 Hook 是以 use 开头的函数,通过自定义 Hook 函数可以用来实现逻辑的封装和复用

封装思路:

  1. 声明一个以 use 开头的函数

  2. 在函数体内封装可复用的逻辑(只要是可复用的逻辑)

  3. 返回状态和回调(以对象或者数据返回)

  4. 在哪个组件中要用到,就执行这个函数,解构出状态和回调即可使用

import React, {useEffect, useState} from "react";

// 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用
// 解决思路:自定义 hook

function useToggle() {
    // 可复用的代码逻辑
    const [value, setValue] = useState(true);

    const toggle = () => setValue(!value);
    // 哪些状态和回调函数需要在其他组件使用 return
    return {
        value,
        toggle
    }
}

function App() {

    const { value, toggle } = useToggle();

    return (
        <div className="App">
            {value && <div>this is div</div>}
            <button onClick={toggle}>toggle</button>
        </div>
    );
}

export default App;

15. ReactHooks 使用规则

  1. 只能在组件中或者其他自定义Hook函数中调用

  2. 只能在组件的顶层调用,不能嵌套在 if、for、其他函数中

import React, {useEffect, useState} from "react";


// 问题:布尔切换逻辑与当前组件耦合在一起,不方便使用
// 解决思路:自定义 hook

function useToggle() {
    // 可复用的代码逻辑
    const [value, setValue] = useState(true);

    const toggle = () => setValue(!value);
    // 哪些状态和回调函数需要在其他组件使用 return
    return {
        value,
        toggle
    }
}

function App() {

    const { value, toggle } = useToggle();

    return (
        <div className="App">
            {value && <div>this is div</div>}
            <button onClick={toggle}>toggle</button>
        </div>
    );
}

export default App;

16. B 站评论优化

模拟请求评论接口,抽象出 Hook

实现思路:

1.使用 json-server 工具模拟接口服务,通过 axios 发送接口请求

2.使用 useEffect 调用接口获取数据

安装 json-server 库:

npm i json-server -D

安装 axios 库:

npm install axios

db.json:

{
  "list": [
    {
      "rpid": 3,
      "user": {
        "uid": "13258165",
        "avatar": "http://toutiao.itheima.net/resources/images/98.jpg",
        "uname": "周杰伦"
      },
      "content": "哎哟,不错哦",
      "ctime": "10-18 08: 15",
      "like": 126
    },
    {
      "rpid": 2,
      "user": {
        "uid": "36080105",
        "avatar": "http://toutiao.itheima.net/resources/images/98.jpg",
        "uname": "许嵩"
      },
      "content": "我寻你千百度 日出到迟暮",
      "ctime": "11-13 11: 29",
      "like": 88
    },
    {
      "rpid": 1,
      "user": {
        "uid": "30009257",
        "avatar": "http://toutiao.itheima.net/resources/images/98.jpg",
        "uname": "黑马前端"
      },
      "content": "学前端就来黑马",
      "ctime": "10-19 09: 00",
      "like": 66
    }
  ]
}

改写 package.json:

  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "serve": "json-server db.json --port 3004"
  },

App.js:

import './App.scss'
import avatar from './images/bozai.png'
import {useEffect, useRef, useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
import {v4 as uuidV4} from 'uuid'
import dayjs from "dayjs";
import axios from "axios";

/**
 * 评论列表的渲染和操作
 *
 * 1. 根据状态渲染评论列表
 * 2. 删除评论
 */

// 评论列表数据
const list = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-20 08:15',
    like: 38,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '09-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '黑马前端',
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
  },
]


// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar,
  // 用户昵称
  uname: '黑马前端',
}

/**
 * 导航 Tab 的渲染和操作
 *
 * 1. 渲染导航 Tab 和高亮
 * 2. 评论列表排序
 *  最热 => 喜欢数量降序
 *  最新 => 创建时间降序
 */

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

const App = () => {

  // 渲染评论列表
  // 1.使用 useState 维护 list
  // const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));

  // 获取接口数据渲染
  const [commentList, setCommentList] = useState([]);

  useEffect(  () => {
    // 请求数据
    async function getList() {
      // axios 请求数据
      const res = await axios.get('http://localhost:3004/list');
      setCommentList(res.data);
    }
    getList();
  }, []);

  const inputRef = useRef(null);

  // 删除功能
  const handleDel = (id) => {
    // 对 commentList 进行过滤
    setCommentList(commentList.filter(item => item.rpid !== id));
  };

  // tab 切换功能
  // 1.点击谁就把谁的 type 记录下来
  // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
  const [type, setType] = useState('hot');
  const handleTabChange = (type) => {
    console.log(type);
    setType(type);
    // 基于列表的排序
    if (type === 'hot') {
      // 根据点赞数量排序
      // lodash
      setCommentList(_.orderBy(commentList, 'like', 'desc'));
    } else {
      // 根据创建时间排序
      setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
    }
  };

  // 发表评论
  const [content, setContent] = useState('');
  const handlePublish = () => {
    setCommentList([
      ...commentList,
      {
        rpid: uuidV4(),
        user: {
          uid: '30009257',
          avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
          uname: '黑马前端',
        },
        content: content,
        ctime: dayjs(new Date()).format('MM-DD hh:mm'),
        like: 80,
      }
      ]);
    // 1.清楚输入框内容
    setContent('')
    // 2.重新聚焦
    inputRef.current.focus();
  };

  return (
    <div className="app">
      {/* 导航 Tab */}
      <div className="reply-navigation">
        <ul className="nav-bar">
          <li className="nav-title">
            <span className="nav-title-text">评论</span>
            {/* 评论数量 */}
            <span className="total-reply">{10}</span>
          </li>
          <li className="nav-sort">
            {/* 高亮类名: active */}
            {tabs.map(item =>
                <span key={item.type}
                      onClick={() => handleTabChange(item.type)}
                      className={classNames('nav-item', {active: type === item.type})}>
                  {item.text}
                </span>)
            }
          </li>
        </ul>
      </div>

      <div className="reply-wrap">
        {/* 发表评论 */}
        <div className="box-normal">
          {/* 当前用户头像 */}
          <div className="reply-box-avatar">
            <div className="bili-avatar">
              <img className="bili-avatar-img" src={avatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
              value={content}
              onChange={(e) => setContent(e.target.value)}
              ref={inputRef}
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <div className="send-text" onClick={handlePublish}>发布</div>
            </div>
          </div>
        </div>
        {/* 评论列表 */}
        <div className="reply-list">
          {/* 评论项 */}
          {commentList.map(item => (
              <div key={item.rpid} className="reply-item">
                {/* 头像 */}
                <div className="root-reply-avatar">
                  <div className="bili-avatar">
                    <img
                        className="bili-avatar-img"
                        alt=""
                        src={item.user.avatar}
                    />
                  </div>
                </div>

                <div className="content-wrap">
                  {/* 用户名 */}
                  <div className="user-info">
                    <div className="user-name">{item.user.uname}</div>
                  </div>
                  {/* 评论内容 */}
                  <div className="root-reply">
                    <span className="reply-content">{item.content}</span>
                    <div className="reply-info">
                      {/* 评论时间 */}
                      <span className="reply-time">{item.ctime}</span>
                      {/* 评论数量 */}
                      <span className="reply-time">点赞数:{item.like}</span>
                      {/*条件:user.id === item.user.id*/}
                      {user.uid === item.user.uid  && <span className="delete-btn" onClick={() => handleDel(item.rpid)}>删除</span>}
                    </div>
                  </div>
                </div>
              </div>
          ))}

        </div>
      </div>
    </div>
  )
}

export default App;

封装评论项 Item 组件

import './App.scss'
import avatar from './images/bozai.png'
import {useEffect, useRef, useState} from "react";
import _ from 'lodash';
import classNames from "classnames";
import {v4 as uuidV4} from 'uuid'
import dayjs from "dayjs";
import axios from "axios";

/**
 * 评论列表的渲染和操作
 *
 * 1. 根据状态渲染评论列表
 * 2. 删除评论
 */

// 评论列表数据
const list = [
  {
    // 评论id
    rpid: 3,
    // 用户信息
    user: {
      uid: '13258165',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '周杰伦',
    },
    // 评论内容
    content: '哎哟,不错哦',
    // 评论时间
    ctime: '10-20 08:15',
    like: 38,
  },
  {
    rpid: 2,
    user: {
      uid: '36080105',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '许嵩',
    },
    content: '我寻你千百度 日出到迟暮',
    ctime: '09-13 11:29',
    like: 88,
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
      uname: '黑马前端',
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
  },
]


// 当前登录用户信息
const user = {
  // 用户id
  uid: '30009257',
  // 用户头像
  avatar,
  // 用户昵称
  uname: '黑马前端',
}

/**
 * 导航 Tab 的渲染和操作
 *
 * 1. 渲染导航 Tab 和高亮
 * 2. 评论列表排序
 *  最热 => 喜欢数量降序
 *  最新 => 创建时间降序
 */

// 导航 Tab 数组
const tabs = [
  { type: 'hot', text: '最热' },
  { type: 'time', text: '最新' },
]

// 封装请求数据的 Hook
function useGetList() {
  // 获取接口数据渲染
  const [commentList, setCommentList] = useState([]);

  useEffect(  () => {
    // 请求数据
    async function getList() {
      // axios 请求数据
      const res = await axios.get('http://localhost:3004/list');
      setCommentList(res.data);
    }
    getList();
  }, []);
  return {
    commentList,
    setCommentList
  };
}

// 评论项组件
function Item({ item, onDel }) {
  return (
      <div className="reply-item">
        {/* 头像 */}
        <div className="root-reply-avatar">
          <div className="bili-avatar">
            <img
                className="bili-avatar-img"
                alt=""
                src={item.user.avatar}
            />
          </div>
        </div>

        <div className="content-wrap">
          {/* 用户名 */}
          <div className="user-info">
            <div className="user-name">{item.user.uname}</div>
          </div>
          {/* 评论内容 */}
          <div className="root-reply">
            <span className="reply-content">{item.content}</span>
            <div className="reply-info">
              {/* 评论时间 */}
              <span className="reply-time">{item.ctime}</span>
              {/* 评论数量 */}
              <span className="reply-time">点赞数:{item.like}</span>
              {/*条件:user.id === item.user.id*/}
              {user.uid === item.user.uid  && <span className="delete-btn" onClick={() => onDel(item.rpid)}>删除</span>}
            </div>
          </div>
        </div>
      </div>
  )
}

const App = () => {

  // 渲染评论列表
  // 1.使用 useState 维护 list
  // const [commentList, setCommentList] = useState(_.orderBy(list, 'like', 'desc'));

  const { commentList, setCommentList } = useGetList();

  const inputRef = useRef(null);

  // 删除功能
  const handleDel = (id) => {
    // 对 commentList 进行过滤
    setCommentList(commentList.filter(item => item.rpid !== id));
  };

  // tab 切换功能
  // 1.点击谁就把谁的 type 记录下来
  // 2.通过记录 type 和每一项遍历时的 type 做匹配 控制激活类名的显示
  const [type, setType] = useState('hot');
  const handleTabChange = (type) => {
    console.log(type);
    setType(type);
    // 基于列表的排序
    if (type === 'hot') {
      // 根据点赞数量排序
      // lodash
      setCommentList(_.orderBy(commentList, 'like', 'desc'));
    } else {
      // 根据创建时间排序
      setCommentList(_.orderBy(commentList, 'ctime', 'desc'))
    }
  };

  // 发表评论
  const [content, setContent] = useState('');
  const handlePublish = () => {
    setCommentList([
      ...commentList,
      {
        rpid: uuidV4(),
        user: {
          uid: '30009257',
          avatar: 'https://www.keaitupian.cn/cjpic/frombd/0/253/936677050/470164789.jpg',
          uname: '黑马前端',
        },
        content: content,
        ctime: dayjs(new Date()).format('MM-DD hh:mm'),
        like: 80,
      }
      ]);
    // 1.清楚输入框内容
    setContent('')
    // 2.重新聚焦
    inputRef.current.focus();
  };

  return (
    <div className="app">
      {/* 导航 Tab */}
      <div className="reply-navigation">
        <ul className="nav-bar">
          <li className="nav-title">
            <span className="nav-title-text">评论</span>
            {/* 评论数量 */}
            <span className="total-reply">{10}</span>
          </li>
          <li className="nav-sort">
            {/* 高亮类名: active */}
            {tabs.map(item =>
                <span key={item.type}
                      onClick={() => handleTabChange(item.type)}
                      className={classNames('nav-item', {active: type === item.type})}>
                  {item.text}
                </span>)
            }
          </li>
        </ul>
      </div>

      <div className="reply-wrap">
        {/* 发表评论 */}
        <div className="box-normal">
          {/* 当前用户头像 */}
          <div className="reply-box-avatar">
            <div className="bili-avatar">
              <img className="bili-avatar-img" src={avatar} alt="用户头像" />
            </div>
          </div>
          <div className="reply-box-wrap">
            {/* 评论框 */}
            <textarea
              className="reply-box-textarea"
              placeholder="发一条友善的评论"
              value={content}
              onChange={(e) => setContent(e.target.value)}
              ref={inputRef}
            />
            {/* 发布按钮 */}
            <div className="reply-box-send">
              <div className="send-text" onClick={handlePublish}>发布</div>
            </div>
          </div>
        </div>
        {/* 评论列表 */}
        <div className="reply-list">
          {/* 评论项 */}
          {commentList.map(item => <Item item={item} onDel={handleDel} />)}

        </div>
      </div>
    </div>
  )
}

export default App;

17. Redux 集中状态管理工具

Redux 是 React 最常用的集中状态管理工具,类似于 Vue 中的 Pinia(Vuex),可以独立于框架运行

作用:通过集中管理的方式管理应用的状态

使用步骤:

  1. 定义一个 Redux 函数

  2. 使用 createStore 方法传入 reducer 函数,生成一个 store 实例对象

  3. 使用 store 实例的 subscribe 方法订阅数据的变化(数据一旦变化,可以得到通知)

  4. 使用 store 实例的 dispatch方法提交 action 对象触发数据变化(告诉 reducer 你想怎么改数据)

  5. 使用 store 实例的 getState 方法获取最新的状态数据更新到视图中

Redux 与 React环境准备

配套工具

  1. Redux Toolkit — 官方推荐编写 Redux 逻辑的方式,是一套工具的集合,简化书写方式

    • 简化 store 配置

    • 内置 immer 支持可变式状态修改

    • 内置 thunk,更好的异步创建

  2. react-redux — 用来链接 Redux 和 React 组件的中间件

    • 获取状态,更新状态

配置基础环境

  1. 使用CRA快速创建React项目

npx create-react-app react-redux
  1. 安装配套工具

npm i @reduxjs/toolkit react-redux
  1. 启动项目

npm run start

使用 React Toolkit 创建 counterStore

counterStore.js:

import {createSlice} from "@reduxjs/toolkit";

const counterStore = createSlice({
    // store 名称
    name: 'counter',
    // 初始化状态
    initialState: {
        count: 0
    },
    // 修改状态的方法,同步方法,支持直接修改
    reducers: {
        increment(state) {
            state.count++;
        },
        decrement(state) {
            state.count--;
        }
    }
});

// 结构出来 actionCreator 函数
const {increment, decrement} = counterStore.actions;

const reducer = counterStore.reducer;

// 按需导出 actionCreator
export {increment, decrement};

// 默认导出 reducer
export default reducer;

src\store\index.js:

import {configureStore} from "@reduxjs/toolkit";
import counterStore from "./modules/counterStore";


const store = configureStore({
    reducer: {
        counter: counterStore
    }
});

export default store;

src\index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {Provider} from "react-redux";
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
      <Provider store={store}>
          <App />
      </Provider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

React 组件使用 store 中的数据

const { count } = useSelector(state => state.counter);

React 组件修改 store 中的数据

import './App.css';
import {useDispatch, useSelector} from "react-redux";
// 导入 actionCreator
import {decrement, increment} from "./store/modules/counterStore";

function App() {
  const { count } = useSelector(state => state.counter);
  const dispatch = useDispatch();

  return (
    <div className="App">
      <button onClick={() => dispatch(decrement())}>-</button>
      <span>{count}</span>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  );
}

export default App;

总结:

image-20240713175536468

提交 action 修改 state 值

counterStore.js:

import {createSlice} from "@reduxjs/toolkit";

const counterStore = createSlice({
    // store 名称
    name: 'counter',
    // 初始化状态
    initialState: {
        count: 0
    },
    // 修改状态的方法,同步方法,支持直接修改
    reducers: {
        increment(state) {
            state.count++;
        },
        decrement(state) {
            state.count--;
        },
        addToNum(state, action) {
            state.count += action.payload;
        }
    }
});

// 结构出来 actionCreator 函数
const {increment, decrement, addToNum} = counterStore.actions;

const reducer = counterStore.reducer;

// 按需导出 actionCreator
export { increment, decrement, addToNum };

// 默认导出 reducer
export default reducer;

App.js:

import './App.css';
import {useDispatch, useSelector} from "react-redux";
// 导入 actionCreator
import {addToNum, decrement, increment} from "./store/modules/counterStore";

function App() {
  const { count } = useSelector(state => state.counter);
  const dispatch = useDispatch();
	
  return (
    <div className="App">
          <button onClick={() => dispatch(decrement())}>-</button>
          <span>{count}</span>
          <button onClick={() => dispatch(increment())}>+</button>
        <button onClick={() => dispatch(addToNum(10))}>add to 10</button>
        <button onClick={() => dispatch(addToNum(20))}>add to 20</button>
    </div>
  );
}

export default App;

Redux 异步状态操作

异步操作样板:

  1. 创建stor的写法保持不变,配置好同步修改状态的方法

  2. 单独封装一个函数,在函数内部return一个新函数,在新函数中

    1. 封装异步请求获取数据

    2. 调用同步action Creater传入异步数据生成一个action对象,并使用dispatch提交

  3. 组件中dispatch的写法保特不变

channelStore.js:

import {createSlice} from "@reduxjs/toolkit";
import axios from "axios";


const channelStore = createSlice({
    // store 名称
    name: 'channel',
    initialState: {
        channelList: []
    },
    reducers: {
        setChannels(state, action) {
            state.channelList = action.payload;
        }
    }
});


// 异步请求部分
const { setChannels } = channelStore.actions;

const fetchChannelList = () => {
    return async (dispatch) => {
        const res = await axios.get('http://geek.itheima.net/v1_0/channels');
        dispatch(setChannels(res.data.data.channels));
    };
};

export { fetchChannelList };

const reducer = channelStore.reducer;

export default reducer;

index.js:

import {configureStore} from "@reduxjs/toolkit";
import counterStore from "./modules/counterStore";
import channelStore from "./modules/channelStore";

const store = configureStore({
    reducer: {
        counter: counterStore,
        channel: channelStore,
    }
});

export default store;

App.js:

import {useDispatch, useSelector} from "react-redux";
// 导入 actionCreator
import {addToNum, decrement, increment} from "./store/modules/counterStore";
import {useEffect} from "react";
import {fetchChannelList} from "./store/modules/channelStore";

function App() {
  const { count } = useSelector(state => state.counter);
  const { channelList } = useSelector(state => state.channel);
  const dispatch = useDispatch();

  useEffect(() => {
      dispatch(fetchChannelList());
  }, [dispatch]);

  return (
    <div className="App">
          <button onClick={() => dispatch(decrement())}>-</button>
          <span>{count}</span>
          <button onClick={() => dispatch(increment())}>+</button>
        <button onClick={() => dispatch(addToNum(10))}>add to 10</button>
        <button onClick={() => dispatch(addToNum(20))}>add to 20</button>
        <ul>
            {channelList.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
    </div>
  );
}

export default App;

18. React Router 路由

上手 demo

需求:创建一个可以切换登录和文章页的路由

实现:

npm i react-router-dom # 安装路由依赖

index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import {createBrowserRouter, RouterProvider} from "react-router-dom";

// 1. 创建 Router 实例对象并且配置路由对应关系
const router = createBrowserRouter([
    {
        path: '/login',
        element: <div>我是登录页</div>
    },
    {
        path: '/article',
        element: <div>我是文章页</div>
    },
]);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router}></RouterProvider>
  </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

实际开发中的 router 配置

1.创建 page 文件夹,分别创建 Login 和 Article 目录,再分别创建 index.js 文件

src/page/article/index.js:

const Article = () => {
    return <div>我是文章</div>
};

export default Article;

src/page/login/index.js:

const Login = () => {
    return <div>我是登录</div>
};

export default Login;

2.创建 router 文件夹,在其中创建 index.js 文件

router/index.js:

import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";

const router = createBrowserRouter([
    {
        path: '/login',
        element: <Login />
    },
    {
        path: '/article',
        element: <Article />
    }
]);

export default router;

3.在 src/index.js 文件中引入 router 实例

src/index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import {createBrowserRouter, RouterProvider} from "react-router-dom";

import router from './router';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router}></RouterProvider>
  </React.StrictMode>
);

reportWebVitals();

路由导航

声明式导航

指的是在模板中通过 <Link /> 组件描述要调到哪里去,常用于 Tab 栏。类似 Vue 的 router-link

示例:

<Link to="/article">文章</Link>
编程式导航

编程式导航是指通过useNavigate钩子得到导航方法,然后通过调用方法以命令式的形式进行路由跳转,比如想在 登录请求完毕之后跳转就可以选择这种方式,更加灵活

示例:

import {Link, useNavigate} from "react-router-dom";

const Login = () => {
    const navigate = useNavigate();
    return (
        <div>
            <div>我是登录</div>
            <Link to='/article'>跳转文章页</Link>
            <button onClick={() => navigate('/article')}>跳转文章页</button>
        </div>
    )
};

export default Login;

路由传参

searchParams 传参
// 传参
const navigate = useNavigate();
<button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button>

// 拿参
const [params] = useSearchParams();
const id = params.get('id');
<div>我是文章页{id}</div>
params 传参
// 传参
const navigate = useNavigate();
<button onClick={() => navigate('/article/1001')}>params 传参</button>

// 取参
const params = useParams();
const id = params.id;
<div>我是文章页{id}</div>

注意 parmas 传参必须要在 router/index.js 文件的 path 加上 :id

import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";

const router = createBrowserRouter([
    {
        path: '/login',
        element: <Login />
    },
    {
        path: '/article/:id',
        element: <Article />
    }
]);

export default router;

传参多值:

// 传参
const navigate = useNavigate();
<button onClick={() => navigate('/article?id=1001&name=jack')}>searchParams 传参</button>
<button onClick={() => navigate('/article/1001/hejiajun')}>params 传参</button>

// 取参
const params = useParams();
const id = params.id;
const name = params.name;
<div>我是文章页{id}</div>
<div>我是文章页{name}</div>

router/index.js:

import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";

const router = createBrowserRouter([
    {
        path: '/login',
        element: <Login />
    },
    {
        path: '/article/:id/:name',
        element: <Article />
    }
]);

export default router;

嵌套路由配置

大致步骤:

  1. 使用 children 属性配置路由嵌套关系

  2. 使用 <Outlet /> 组件配置二级路由渲染位置

router/index.js:

import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
import Layout from "../page/layout";
import Board from "../page/board";
import About from "../page/about";

const router = createBrowserRouter([
    {
        path: '/',
        element: <Layout />,
        children: [
            {
                path: 'board',
                element: <Board />
            },
            {
                path: 'about',
                element: <About />
            }
        ]
    },
    {
        path: '/login',
        element: <Login />
    },
    {
        path: '/article/:id/:name',
        element: <Article />
    }
]);

export default router;

src/page/layout/index.js:

import {Link, Outlet} from "react-router-dom";

const Layout = () => {
    return (
        <div>
            我是一级路由 layout 组件
            <Link to='/board'>面板</Link>
            <Link to='/about'>关于</Link>
            {/*配置二级路由的出口*/}
            <Outlet />
        </div>
    )
};

export default Layout;

默认二级路由设置

当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置 index 属性为 true

src/router/index.js

import {createBrowserRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
import Layout from "../page/layout";
import Board from "../page/board";
import About from "../page/about";

const router = createBrowserRouter([
    {
        path: '/',
        element: <Layout />,
        children: [
            // 设置默认二级路由,一级路由被访问的时候,它也能得到渲染
            {
                index: true,
                element: <Board />
            },
            {
                path: 'about',
                element: <About />
            }
        ]
    },
    {
        path: '/login',
        element: <Login />
    },
    {
        path: '/article/:id/:name',
        element: <Article />
    }
]);

export default router;

404 路由配置

场景:当浏览器输入url的路径在整个路由配置中都找不到对应的pth,为了用户体验,可以使用404兜底组件进行 渲染

两种路由模式

image-20240714111532334

创建 hash 路由

import {createHashRouter} from "react-router-dom";
import Login from "../page/login";
import Article from "../page/article";
import Layout from "../page/layout";
import Board from "../page/board";
import About from "../page/about";
import NotFound from "../page/notfound";

const router = createHashRouter([
    {
        path: '/',
        element: <Layout />,
        children: [
            // 设置默认二级路由,一级路由被访问的时候,它也能得到渲染
            {
                index: true,
                element: <Board />
            },
            {
                path: 'about',
                element: <About />
            }
        ]
    },
    {
        path: '/login',
        element: <Login />
    },
    {
        path: '/article/:id/:name',
        element: <Article />
    },
    {
        path: '*',
        element: <NotFound />
    }
]);

export default router;

19. 记账本

这边没有认真记,想要学习的小伙伴可以直接看原视频好了,配合视频和笔记学习效果会更好。

环境搭建

使用CRA创建项目,并安装必要依赖,包括下列基础包

npx create-react-app react-bill
  1. Redux:状态管理-@reduxjs./toolkit、react-redux

  2. 路由-reac-router-dom

  3. 时间处理-dayjs

  4. class:类名处理-classnames

  5. 移动端组件库-antd-mobile

  6. 请求插件-axios

npm i @reduxjs/toolkit react-redux react-router-dom dayjs classnames antd-mobile axios

配置别名路径

1.路径解析配置,把 @/ 解析为 src/ (1. npm i -D @craco/craco 2.项目根目录下创建配置文件craco.config.js

2.路径联想配置(VsCode),VsCode在输入@/时,自动联想出来对应的src/下的子级目录

数据 Mock

npm i -D json-server

20. UseReducer

作用:和 useState 作用类似,用来管理相对复杂的状态数据

基础用法:

  1. 定义一个reducer函数(根据不同的action返回不同的新状态)

  2. 在组件中调用useReducer,并传入reducer函数和状态的初始值

  3. 事件发生时,通过 dispatch 函数分派一个 action 对象(通知 reducer 要返回哪个新状态并渲染 UI)

App.js:

import React, {useReducer} from "react";

// useReducer
// 1. 定义 reducer 函数,根据不同的action返回不同的新状态
// 2. 在组件中调用 useReducer,并传入 reducer 函数和状态的初始值
// 3. 调用 dispatch 通知 reducer 产生一个新状态,利用新状态更新 UI

function reducer(state, action) {
    switch (action.type) {
        case 'INC':
            return state + 1;
        case 'DEC':
            return state - 1;
        case 'SET':
            return action.payload;
        default:
            return state;
    }

}

function App() {

    const [state, dispatch] = useReducer(reducer, 0);

    return (
        <div className="App">
            this is app
            <button onClick={() => dispatch({ type: 'DEC' })}>-</button>
            {state}
            <button onClick={() => dispatch({ type: 'INC' })}>+</button>
            <button onClick={() => dispatch({ type: 'SET', payload: 100 })}>update</button>
        </div>
    );
}

export default App;

21. useMemo

需求:

image-20240714123347346

作用:在组件每次渲染的时候缓存计算结果

useMemo(() => {
    // 根据 count1 返回计算的结果
}, [count1])

说明:使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算。接受两个参数,第一个是函数,第二个是依赖项。

App.js:

import React, {useMemo, useState} from "react";

function fib(n) {
    console.log('计算函数执行了');
    if (n < 3) {
        return 1;
    }
    return fib(n - 2) + fib(n - 1);
}

function App() {
    const [count1, setCount1] = useState(0);

    const result = useMemo(() => {
        return fib(count1);
    }, [count1]);

    // const result = fib(count1);
    const [count2, setCount2] = useState(0);
    console.log('组件重新渲染了');
    return (
        <div className="App">
            this is app
            <button onClick={() => setCount1(count1 + 1)}>change count1: {count1}</button>
            <button onClick={() => setCount2(count2 + 1)}>change count2: {count2}</button>
            result is {result}
        </div>
    );
}

export default App;

使用场景:消耗非常大的计算

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值