React全家桶 - 【React】 - 【2】组件基础(组件定义及使用、useState、受控表单组件、组件样式、useRef)

前言

什么是组件?

  • 组件是一个广泛的概念,现在流行的框架中都有组件;
  • 一个组件就是用户界面的一部分,它可以有自己 的 逻辑 和 外观组件之间 可以 相互嵌套 ,也可以 复用 多次

一、React组件

1.1 基本概念 及 注意事项

  • 在 React 中,一个 组件 就是 首字母 大写 的 函数 ,内部存放了 组件的 逻辑 和 视图UI,渲染组件只需要把组件 当成 标签 书写 即可;
  • 注意
    • React组件是常规的JS函数,但 组件的名称 必须以 大写字母 开头,否则它们将无法运行;
    • React组件 必须 要有一个 根标签
    • React组件函数也可以是箭头函数;
    • 如果你的标签和return关键字不在同一行,则必须把它包裹在一对括号中(小括号);
      • 没有括号包裹的话,任何在 return 下一行的代码 都将被忽略
    • 所有的标签都必须是闭合标签;
      • 如果是单标签:必须自闭合;

1.2 定义组件

jsx 复制代码
// 1. 定义函数
function Button() {
    // 组件内部逻辑
    // 2. 添加标签
    return <button> click me </button>
}

// 3. 导出组件
export default Button;

1.3 使用组件(渲染组件)

  • 正常的定义组件都是以上三个步骤,这里为了简单方便,就直接在 App.js 中定义组件;
jsx 复制代码
// 定义组件
const Button = () => {
  // 业务逻辑 及 组件逻辑
  const onClick = (name, e) => {
    console.log(name, e);
  };
  return <button onClick={(e) => onClick('禁止摆烂_才浅', e)}>Click Me</button>;
}

// 使用组件
function App() {
  return (
    <div>
      {/* 单标签  ===>  自闭合 */}
      <Button />
      
      {/* 双标签 */}
      <Button></Button>
    </div>
  );
}

export default App;

二、useState 基础使用

2.1 基本介绍

  • useState是一个 React Hook(函数),它允许我们向组件添加一个 状态变量,从而控制影响组件的渲染结果;
  • 本质
    • 和普通JS变量不同的是,状态变量一旦发生变化,组件的视图UI也会跟着变化(数据驱动试图);
    • 就比如说,我将 状态变量 count 的值 从 0 => 1,那么视图上的显示结果也会从 0 => 1;

2.2 语法

  • 注意
    • 使用之前需要先导入;
    • React钩子 useState 不能在顶层被调用;
    • React Hooks 必须在 React函数组件自定义React Hook函数 中 调用
jsx 复制代码
import { useState } from 'react';

const [状态变量, 修改状态变量的函数] = useState(初始值);
// const [状态变量, set状态变量] = useState(初始值);
  • useState 是一个函数返回值 是 一个 数组

  • 数组中的:

    • 第一个参数 :是 状态变量
    • 第二个参数 :是 set函数,用来修改 状态变量 的 值
      • 该函数的参数就是状态变量修改之后的值(想让状态变量变成多少,就写多少);
      • 调用该函数的作用
        • 修改 状态变量 的值(用传入的新值 替换 旧值);
        • 重新使用新的 状态变量 渲染视图
  • useState参数 将作为 状态变量初始值

  • 代码展示:

    jsx 复制代码
    import { useState } from 'react';
    
    const Button = () => {
      // 调用 useState 添加一个状态变量
      // num ===> 状态变量
      // setNum ===> 修改状态变量的函数
      const [num, setNum] = useState(0);
    
      // 点击事件 - num自增
      const onClick = () => {
        // 调 setNum 的作用
        // 1. 修改 状态变量 num 的值(用传入的新值修改状态变量)
        // 2. 重新使用新的 状态变量 num 渲染视图
        setNum(num + 1);
      };
      
      return (
        <div>
          <button onClick={onClick}>Click Me</button>
          <br />
          <span>状态变量的值 --- {num}</span>
        </div>
      );
    };
    
    function App() {
      return (
        <div>
          <Button />
        </div>
      );
    }
    
    export default App;

2.3 修改状态的规则

2.3.1 修改 基本数据类型 的状态

  • 在 React 中,状态 被认为是 只读 的,我们应该始终 替换 而不是 修改 它,直接修改 状态 不能引起视图更新
  • 注意
    • 直接修改 状态变量 的 值,状态变量 能被 修改,但是 不会引起 视图 的 更新;
    • 既要 修改状态 变量的值,还想要 视图同时更新,只能通过 useStateset 函数去修改状态变量的值;
  • 代码展示:

    jsx 复制代码
    import { useState } from 'react';
    
    const Button = () => {
      // 调用 useState 添加一个状态变量
      // num ===> 状态变量
      // setNum ===> 修改状态变量的函数
      let [num, setNum] = useState(0);
    
      // 点击事件 - num自增
      const onClick = () => {
        setNum(num + 1);
      };
    
      const onClick1 = () => {
        num++;
      };
      return (
        <div>
          <button onClick={onClick1}>Click Me</button>
          <br />
          <span>直接修改状态变量的值 --- {num}</span>
          <br />
          <hr />
          <br />
          <button onClick={onClick}>Click Me</button>
          <br />
          <span>使用useState的set函数状态变量的值 --- {num}</span>
        </div>
      );
    };
    
    function App() {
      return (
        <div>
          <Button />
        </div>
      );
    }
    
    export default App;
  • 演示效果:

2.3.2 修改 对象、数组 的状态

  • 规则

    • 对于对象类型的状态变量,应该始终传给 set 一个 全新的对象 来进行修改;
  • 代码展示:

    jsx 复制代码
    import { useState } from 'react';
    
    const Button = () => {
      // 对象格式
      const [info, setInfo] = useState({
        name: '张三',
        age: 22,
        gender: '男'
      });
      
      const onClick = () => {
        setInfo({
          ...info,
          name: '李四',
          age: 58
        });
      };
      
      const onClick1 = () => {
        info.name = '王麻子';
        info.age = 44;
        console.log(info);
      };
    
      // 数组形式
      const [numArr, setNumArr] = useState([0, 1, 2, 3, 4]);
    
      const onChange = () => {
        setNumArr([1, 2, 3, 4, 5]);
      };
    
      const onChange1 = () => {
        numArr[0] = 100;
        console.log(numArr);
      };
    
      return (
        <div>
          <button onClick={onClick1}>Click Me</button>
          <br />
          <span>
            直接修改状态变量的值 --- {info.name} - {info.age}
          </span>
          <br />
          <hr />
          <br />
          <button onClick={onClick}>Click Me</button>
          <br />
          <span>
            使用useState的set函数状态变量的值 --- {info.name} - {info.age}
          </span>
          <br />
    
          <hr />
          <br />
          <button onClick={onChange1}>Click Me</button>
          <br />
          <span>直接修改状态变量的值 --- {numArr}</span>
          <br />
          <hr />
          <br />
          <button onClick={onChange}>Click Me</button>
          <br />
          <span>使用useState的set函数状态变量的值 --- {numArr}</span>
        </div>
      );
    };
    
    function App() {
      return (
        <div>
          <Button />
        </div>
      );
    }
    
    export default App;
  • 演示效果:

三、组件的样式处理

  • React组件基础的样式控制有两种控制方案:
    • ✅ class类名控制(单独写样式,导入到组件文件中);
    • ❌ 行内样式(极不推荐);

3.1 ✅ class类名控制

3.1.1 固定类名

  • 代码展示:

    css 复制代码
    .box {
      width: 100px;
      height: 100px;
      background-color: red;
    }
    jsx 复制代码
    import './App.css';
    
    const App = () => {
      return <div className="box box1"></div>;
    };
    
    export default App;
  • 注意
    • 在 React 中,使用类名的时候,需要使用 className 关键字 替代之前的 class

3.1.2 动态 添加 或 删除 类名

动态判断添加 单类名

jsx 复制代码
<div className={item.readState === 0 ? 'no-read' : null}></div>

已有多类名,动态判断再添加类型

jsx 复制代码
// 数组方法
<div className={['box', classA, item.readState === 0 ? 'no-read' : null].join(' ')}></div>
<div className={['box', classA, item.readState === 0 && 'no-read'].join(' ')}></div>

// 模板字符串方法
<div className={`box ${classA} ${item.readState === 0 ? 'no-read' : null}`}></div>
<div className={`box ${classA} ${item.readState === 0 && 'no-read}`}></div>
  • 注意
    • 数组方法时:
      • 要使用 空格 将数组转为字符串;
    • 模板字符串方法时:
      • 类名之间 必须要有 空格

✅ 使用 classnames 依赖

在实际开发中,我们通常需要根据某个条件去判断类名,此时我们可以使用 classnames 这个第三方包进行设置;

js 复制代码
// 安装依赖
npm i classnames
jsx 复制代码
import classNames from 'classnames';
<div className={classNames('box', {'no-read': item.readState === 0 })}></div>

3.2 ❌ 行内样式

  • 行内样式有两种方案:
    • 直接将样式写在行内;
    • 将样式属性写在一个对象中,将这个对象绑定到对应的元素上;
  • 代码展示:
    • 将样式写在行内:

      jsx 复制代码
      const App = () => {
          return <div style={{ width: '100px', height: '100px', backgroundColor: 'red' }}>Hello World</div>;
      };
      
      export default App;
    • 使用对象:

      jsx 复制代码
      const style = {
          width: '100px',
          height: '100px',
          backgroundColor: 'red',
          color: '#fff'
      };
      
      const App = () => {
          return <div style={style}>Hello World</div>;
      };
      
      export default App;

四、受控表单组件绑定

4.1 概念

  • 使用 React 组件状态的状态(useState)控制表单的状态;

4.2 使用步骤

  • 准备一个React状态值

    • 使用useState声明状态;
    jsx 复制代码
    const [value, setValue] = useState('');
  • 通过 value 属性绑定状态,通过onChange事件绑定状态同步的函数,通过事件对象e拿到输入框最新的值,反向修改react的状态;

    jsx 复制代码
    <input
      type="text"
      value={value}
      onChange={(e) => setValue(e.target.value)}/>
  • ❗ 注意
    • 当给表单元素设置 value 属性的时候,这个字段呈现一个 只读字段
    • 如果该字段应该是可变的,则使用 defaultValue
    • 否则,设置 onChangereadOnly;

五、获取DOM元素

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

    • 使用 useRef 创建 ref 对象,并于 JSX 绑定;

      • 在组建内部调用;
      jsx 复制代码
      const inputRef = useRef(null);
      
      <input type="text" ref={inputRef} />
  • 在DOM可用时,通过 ref名称.current 拿到DOM对象;

    jsx 复制代码
    console.log(inputRef.current);
  • ❗ 什么是DOM可用?
    • 组件渲染完毕之后才可用的;
    • 渲染之前获取DOM得到的是 null

六、案例展示 - B站评论 - 普通版

  • 效果展示:
  • 功能需求:
    • 渲染评论列表;
    • 实现删除评论;
      • 只有自己的评论才显示删除按钮;
      • 点击删除按钮,删除当前评论,列表中不再显示;
    • 渲染导航Tab和高亮实现;
    • 评论列表排序功能实现;
      • 最新:评论列表按照创建时间排序(新的在前);
      • 最热:点赞数排序(点赞数多的在前);
    • 实现评论功能;
      • 点击发布之后,需要清空输入框的内容,并且自动获取焦点;
      • 回车也可以发送评论;
  • 核心思路:
    • 使用 useState 维护评论列表;
    • 使用 map 对列表进行遍历渲染(一定要添加key);
  • 代码展示:
jsx 复制代码
import { useRef, useState } from 'react';
// 需要安装 lodash
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: '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: '11-13 11:29',
    like: 88,
    action: 2
  },
  {
    rpid: 1,
    user: {
      uid: '30009257',
      avatar,
      uname: '黑马前端'
    },
    content: '学前端就来黑马',
    ctime: '10-19 09:00',
    like: 66,
    action: 1
  }
];

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

// 头部Tab配置项
const tabOptions = [
  { type: 'latest', text: '最新' },
  { type: 'hottest', text: '最热' }
];

const App = () => {
  // 评论列表数据
  const newList = _.orderBy(defaultList, ['ctime'], ['desc']);
  const [list, setList] = useState(newList);
  // 记录活跃的Tab状态
  const [activeTab, setActiveTab] = useState('latest');
  /** 更新评论列表 - 删除 + 排序 */
  const updateList = (type, id) => {
    let newList = [];
    if (type && !['del'].includes(type)) setActiveTab(type);
    switch (type) {
      case 'del':
        newList = list.filter((item) => item.rpid !== id);
        break;
      case 'latest':
        newList = _.orderBy(list, ['ctime'], ['desc']);
        break;
      case 'hottest':
        newList = _.orderBy(list, ['like'], ['desc']);
        break;
      default:
        newList = list;
        break;
    }
    setList(newList);
  };
  /** 保存输入框内容 */
  const [value, setValue] = useState('');
  /** 输入框ref */
  const inputRef = useRef(null);
  /** 发布评论 */
  const addReview = (e) => {
    if (e.key === 'Enter') e.preventDefault();
    if (!value || (e.type === 'keydown' && e.key !== 'Enter')) return;
    const item = {
      rpid: new Date().getTime(),
      user,
      content: value,
      ctime: new Date().toLocaleDateString(),
      like: 0,
      action: 0
    };
    setList([item, ...list]);
    setValue('');
    inputRef.current.focus();
  };

  return (
    <div className="app">
      {/* 头部 */}
      <div className="header align-center">
        <div className="title align-center">
          评论<em>{list.length}</em>
        </div>
        {/* tab栏 */}
        <ul className="tab align-center">
          {tabOptions.map((item, index) => {
            return (
              // 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'}`}
              <li
                className={`item align-center ${activeTab === item.type && 'active'}`}
                onClick={() => updateList(item.type)}
                key={item.type}>
                <span>{item.text}</span>
                {tabOptions.length - 1 !== index && <span className="split-line"></span>}
              </li>
            );
          })}
        </ul>
      </div>

      <div className="review-box">
        {/* 评论框 */}
        <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={value}
              onChange={(e) => setValue(e.target.value)}
              onKeyDown={addReview}
              placeholder="发一条友善的评论"
            />
            <button onClick={addReview}>发布</button>
          </div>
        </div>

        {/* 评论列表 */}
        <div className="review-list">
          {list.map(({ user: { avatar, uname, uid }, content, ctime, like, rpid }) => {
            return (
              <div className="review-item" key={rpid}>
                <div className="left">
                  <img src={avatar} alt={uname} />
                </div>
                <div className="right">
                  <div className="user-name">{uname}</div>
                  <div className="content">{content}</div>
                  <div className="bottom">
                    <div className="time">{ctime}</div>
                    <div className="like-num">点赞数:{like}</div>
                    <ul className="controls">
                      {/* 只有自己的评论才展示删除按钮 */}
                      {uid === user.uid && (
                        <li className="del" onClick={() => updateList('del', rpid)}>
                          删除
                        </li>
                      )}
                    </ul>
                  </div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
};

export default App;
  • bilibili-review.scss

    scss 复制代码
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      background-color: #c9ccd0;
    }
    
    li {
      list-style: none;
    }
    
    em {
      font-style: normal;
    }
    
    .align-center {
      display: flex;
      align-items: center;
    }
    
    .app {
      padding: 100px;
      background-color: #fff;
    
      .header {
        justify-content: flex-start;
        height: 24px;
    
        .title {
          font-weight: bold;
          font-size: 18px;
          color: #333;
    
          em {
            margin-left: 10px;
            color: #666;
            font-size: 12px;
            font-weight: 400;
          }
        }
    
        .tab {
          justify-content: flex-start;
          height: 100%;
          margin-left: 30px;
          color: #666;
          font-size: 12px;
    
          li.item {
            cursor: pointer;
    
            &:hover {
              color: #000;
            }
    
            .split-line {
              height: 11px;
              margin: 0 12px;
              border-right: 1px solid #9499a0;
            }
          }
    
          .active {
            color: #00aeec;
          }
        }
      }
    
      .review-box {
        width: 100%;
        padding-left: 20px;
    
        .post-review {
          align-items: flex-start;
          width: 100%;
          height: 70px;
          margin-top: 16px;
    
          img {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            margin-right: 16px;
          }
    
          .input {
            width: 100%;
            align-items: flex-start;
    
            textarea {
              width: calc(100% - 100px - 10px) !important;
              height: 50px;
              margin-right: 10px;
              padding-left: 10px;
              border: 1px solid #f1f2f3;
              background-color: #f1f2f3;
              border-radius: 6px;
              font-size: 16px;
              line-height: 50px;
              outline: none;
              resize: none;
              transition: height 0.2s;
              appearance: none;
              -webkit-appearance: none;
    
              &:hover,
              &:focus {
                border-color: #c9ccd0;
                background-color: #fff;
              }
    
              &:focus {
                height: 70px;
              }
    
              &::placeholder {
                font-size: 12px;
              }
    
              &::-webkit-scrollbar {
                display: none;
              }
            }
    
            button {
              width: 100px;
              height: 50px;
              background-color: #00aeec;
              border: none;
              border-radius: 6px;
              color: #fff;
              font-size: 15px;
              cursor: pointer;
              opacity: 0.5;
    
              &:hover {
                opacity: 1;
              }
            }
          }
        }
    
        .review-list {
          .review-item {
            display: flex;
            align-items: flex-start;
            width: 100%;
            margin-top: 20px;
    
            .left {
              width: 56px;
              height: 100%;
    
              img {
                width: 40px;
                height: 40px;
                border-radius: 50%;
                cursor: pointer;
              }
            }
    
            .right {
              width: calc(100% - 56px);
              height: 100%;
              padding-bottom: 20px;
              border-bottom: 2px solid #eee;
    
              .user-name {
                color: #61666d;
                font-size: 13px;
                cursor: pointer;
              }
    
              .content {
                margin: 16px 0 10px;
                font-size: 15px;
              }
    
              .bottom {
                display: flex;
                align-items: center;
                justify-content: flex-start;
                color: #9499a0;
                font-size: 13px;
    
                .like-num {
                  margin: 0 20px;
                }
    
                .controls {
                  li {
                    cursor: pointer;
                  }
    
                  .del:hover {
                    color: #000;
                  }
                }
              }
            }
          }
        }
      }
    }
相关推荐
涔溪38 分钟前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇1 小时前
ES6进阶知识一
前端·ecmascript·es6
前端郭德纲1 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR1 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式
帅帅哥的兜兜1 小时前
CSS:导航栏三角箭头
javascript·css3
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss