通过Function函数式方式创建React组件-8

在React中,V16版本之前有三种方式创建组件(createClass() 被删除了),之后只有两种方式创建组件。这两种方式的组件创建方式效果基本相同,但还是有一些区别,这两种方法在体如下:
在这里插入图片描述

本节先了解下用Function函数的方式创建React组件,有了上几节的铺垫,所以本节的代码示例部分无用的代码会被过滤掉以节省篇幅。

通过Function函数方式创建组件(推荐)

代码放在一个单独的.js文件中

//HelloReact.js, 组件文件的名称最好也要大写,以方便与非组件的js区分开
function MyComponent() { //名称要大写
    return (
        <img
            src="https://i.imgur.com/MK3eW3As.jpg"
            alt="Katherine Johnson"
        />
    );
}

export default function Gallery() { //发布组件,可以这样来写
    return (
        <section>
            <h1>了不起的科学家</h1>
            <MyComponent />
            <MyComponent />
        </section>
    );
}

调用

import './App.css';
import Gallery  from "./fun/helloReact";

function Fun() {
  return (
    <div className="fun">
      <h3>HelloReact</h3>
      <Gallery/>
    </div>
  );
}

export default Fun;

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
    <Fun/>
  </React.StrictMode>
);
  • 组件名称,必须以大写字母开头
  • return方法需要注意:如果只一行代码,则可以不写(),而且必须和return在同一行。如果有多行代码必须写(),否则没有括号包裹的话,任何在 return 下一行的代码都 将被忽略!
  • 不要在组件中嵌套组件定义,因为会严重影响性能;

核心属性

props

函数式组件定义参数会有两种写法,用哪一种都可以:

语法格式
  • 第一种:明确定义多个参数,{ }
//这种方式,一定要注意参数外面的的 {}
function Avatar({ person, size }) {
  // ...
}
  • 第二种
function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}
  • 以上两种参数定义的方式的调用方式相同
<Avatar
    person={{ name: 'Lin Lanying', imageId: '1bX5QH6' }}
    size={100}
/>

针对以上两种传值时还可以有如下扩展

设置默认值
export function Avatar1({ person, size=100 }) {
   return (
       <div>{JSON.stringify(person)}, {size}</div>
   );
}
export default function Form({status = 'empty'}) {
    if (status === 'success') {
        return <h1>That's right!</h1>
    }
    return (
        <>
            <h2>City quiz</h2>
        </>
    )
}
传递子元素

注意使用内置的children属性

export function Avatar3(props) {
    return (
        <div>{props.children}</div>
    );
}

//调用方式
<Avatar3>
    <p>我是嵌套组件</p>
</Avatar3>

唯一需要注意的是,props是不可变的,如果是可变的参数要放置在state中。

ref

使用 ref 操作Html原生DOM

由于 React 会自动处理更新 DOM 以匹配你的渲染输出,因此你在组件中通常不需要操作 DOM。但是,有时你可能需要访问由 React 管理的 DOM 元素 —— 例如,让一个节点获得焦点、滚动到它或测量它的尺寸和位置。在 React 中没有内置的方法来做这些事情,所以你需要一个指向 DOM 节点的 ref 来实现。

refref.current

import { useRef } from 'react';

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

useRef()返回一个对象,该对象有一个名为 current 的属性。最初,myRef.current 是 null。当 React 为这个<div>创建一个 DOM 节点时,React 会把对该节点的引用放入 myRef.current。然后,你可以从 事件处理器 访问此 DOM 节点,并使用在其上定义的内置浏览器 API。

使用 ref 操作自定义组件DOM

将 ref 放在你自己的组件上,默认情况下你会得到 null,而且会得到一个错误
Cannot read properties of null (reading 'focus') 。比如下面的代码:

import { useRef } from 'react';

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

但可以用另一种机制forwardRef解决,MyInput 组件是使用 forwardRef 声明的。 这让从上面接收的 inputRef 作为第二个参数 ref 传入组件,第一个参数是 props 。MyInput 组件将自己接收到的 ref 传递给它内部的 <input>就起作用了。

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>
        聚焦输入框
      </button>
    </>
  );
}

事件交互

绑定事件

普通的事件绑定,代码一般写在最上面。这里的handleClick也可以做为一个参数,由父组件传入。

export default function Button() {
    function handleClick() {
        alert('你点击了我!');
    }

    return (
        <button onClick={handleClick}>
            点我
        </button>
    );
}

其它的简单写法,如果不是由父组件传入时,不是太推荐这种写法。因为只有当函数体较短时,内联事件处理函数会很方便。

<button onClick={function handleClick() {
  alert('你点击了我!');
}}>

<button onClick={() => {
  alert('你点击了我!');
}}>

需要注意{handleClick}后面没有(),如果写了括号,在加载组件时就会在渲染时执行而在点击时不会执行。

事件传参

主要用了这个特性,<button onClick={(e)=>handleClick1(e,'tt')}>

export default function Button() {
    function handleClick1(e, args ) {
        console.log(args);
        console.log(e.target);
    }

    return (
        <div>
            <button onClick={handleClick}>
                点我
            </button>
            <button onClick={(e)=>handleClick1(e,'tt')}>
                点我带参数
            </button>
        </div>
    );
}

发送请求

采用内置的post函数即可

 post('/analytics/event', { eventName: 'visit_form' });

将事件处理函数作为 props 传递

就是把点击事件定义在组件外面,这样复用度会更高,下面是一个简单的工具栏的例子:

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
/* 注意这种替代写法
  <button onClick={e => {
    e.stopPropagation();
    onClick();
  }}>
*/
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`正在播放 ${movieName}`);
  }

  return (
    <Button onClick={handlePlayClick}>
      播放 "{movieName}"
    </Button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <PlayButton movieName="魔女宅急便" />
    </div>
  );
}

事件传播和默认行为

这是JS的一个特性,即事件会沿着DOM树向上“冒泡”或“传播”:它从事件发生的地方开始,然后沿着树向上传播。这种机制有好处有坏处,好处是我们可以根据这种特性自动调用外层组件的相同方法。,另一个就是事件捕获,捕获事件对于路由或数据分析之类的代码很有用。

function handleClick(e) {
      e.stopPropagation(); //防止事件传播
      alert('你点击了我!');
  }

另一个相关的就是阻止默认行为,比如表单的onSubmit事件,e.preventDefault();

组件间通信

父子间数据传递

对于父子组件就不细说了,可以查看前一篇文章:

  • 父到子:通过props传递参数;
  • 子到父:通过回调函数更改state通信

跨级组件间通信

通常来说,你会通过 props 将信息从父组件传递到子组件。但是,如果你必须通过许多中间组件向下传递 props,或是在你应用中的许多组件需要相同的信息,传递 props 会变的十分冗长和不便。Context 允许父组件向其下层无论多深的任何组件提供信息,而无需通过 props 显式传递。
在这里插入图片描述
Context 让父组件可以为它下面的整个组件树提供数据,大概原理如下:
在这里插入图片描述

  • 声明 – LevelContext.js’
import { createContext } from 'react';
export const LevelContext = createContext(1); //LevelContext就是参数名,后面1也可以换成一个复杂的对象
  • 使用 – Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
const level = useContext(LevelContext);
  • 提供-- Section.js
    上面其实就完成了一个使用过程,相当于一个全局变量,但还没有传值这么一块。上面代码leavl=1,加了Section.js后level=level的值。
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}> {/*参数*/}
        {children}
      </LevelContext.Provider>
    </section>
  );
}

//调用
<Section level={2}>

这告诉 React:“如果在<Section>组件中的任何子组件请求 LevelContext参数,给他们这个 level(2)”。组件会使用 UI 树中在它上层最近的那个 <LevelContext.Provider> 传递过来的值。

State

React.useState
state的作用不用多说了,使用方式如下:

import { useState } from 'react';

export default function GalleryWall() {
    //index 是一个 state 变量,setIndex 是对应的 setter 函数, 后面的0表示index变量的默认值,定义成const的原因也是为了限制只能通过setIndex()方法来修改,不要直接用==来赋值。
    const [index, setIndex] = useState(0);

    function handleClick() {
        //调用 setIndex 方法相当于改变了state中的数据,然后会自动调用render()函数渲染页面。
        setIndex(index + 1);
    }
    return (
       <div>{index}</div>
    );

在 React 中,useState 以及任何其他以“use”开头的函数都被称为 Hook,作用是可以让组件中使用不同的 React 特性,本质是个函数,但不建议在条件语句、循环语句或其他嵌套函数内调用 Hook。因为这样会比较影响性能。

多个state的写法同上类似

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);
}

使用state重绘UI

这个例子主要是利用了state值变化会自动重新渲染组件的能力,点击按钮后会替换为一个div。

import {useState} from "react";

export default function Repaint() {
    const [isRepainting, setIsRepainting] = useState(false);

    if (isRepainting){
        return(
            <div>重绘了组件UI</div>
        )
    }
    function handleClick(e) {
        setIsRepainting(true); 
    }

    return (
        <div>
            <button onClick={handleClick}>
                点我重绘
            </button>
        </div>
    );
}

state的批处理

这主要是处理特殊情况,原因是state在一次渲染过程中间其值不会变化的。比如下面两行代码。

const [number, setNumber] = useState(0);
//最终值为1,因为在一次渲染过程中虽然调用了三次,但其number值始终为0
setNumber(number + 1);
setNumber(number + 1);
setNumber(number + 1);

//最终值为3,下面是一个固定写法,React的一个队列功能,其中n为自定义的一个变量名
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

更新 state 中的对象

state 中可以保存任意类型的 JavaScript 值,包括对象。但是,你不应该直接修改存放在 React state 中的对象。相反,当你想要更新一个对象时,你需要创建一个新的对象(或者将其拷贝一份),然后将 state 更新为此对象。应该将 state 视为只读的。

const [position, setPosition] = useState({ x: 0, y: 0 });:表示position的初始值为{ x: 0, y: 0 }。

不能直接改变postion的值,应该像下面这样

setPosition({
  x: e.clientX,
  y: e.clientY
});

或是使用如下语法复制一个对象(下面有简写方式):

const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);

如果是表单时,可采用下面的方法,即统一参数名,然后用event.target.name这种方式来更新

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth'
  });

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label> {/**省略lastName/}
      <p>
        {person.firstName}{' '}
      </p>
    </>
  );
}

更新state中的数组

需要注意两个方法:

  • slice 让你可以拷贝数组或是数组的一部分。
  • splice 会直接修改 原始数组(插入或者删除元素)(不能用)

以下是一个添加的例子,注意代码的执行流程:

const [name, setName] = useState('');
const [artists, setArtists] = useState([]);

let nextId = 0;

return (
    <>
        <h1>振奋人心的雕塑家们:</h1>
        <input
            value={name}
            onChange={e => setName(e.target.value)}
        />
        <button onClick={() => {
            setArtists([
                ...artists,
                { id: nextId++, name: name }
            ]);
        }}>添加</button>
        <ul>
            {artists.map(artist => (
                <li key={artist.id}>{artist.name}</li>
            ))}
        </ul>
    </>
);

使用展开…语法复制对象

上面的例子可用下面代码来书写,如果没有后面的x: e.clientX就是一个复制。

setPosition({
  ...position, // 复制上一个 position 中的所有字段
  x: e.clientX // 但是覆盖 x 字段 
});

但需要注意 … 展开语法本质是是“浅拷贝”——它只会复制一层。这使得它的执行速度很快,但是也意味着当你想要更新一个嵌套属性时,你必须得多次使用展开语法。

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

//推荐
setPerson({
  ...person, // 复制其它字段的数据 
  artwork: { // 替换 artwork 字段 
    ...person.artwork, // 复制之前 person.artwork 中的数据
    city: 'New Delhi' // 但是将 city 的值替换为 New Delhi!
  }
});

使用 Immer 插件语法复制对象

npm install use-immer
用 import { useImmer } from 'use-immer' 替换掉 import { useState } from 'react'

import { useImmer } from 'use-immer';

export default function Form() {
    const [person, updatePerson] = useImmer({
        name: 'Niki de Saint Phalle',
        artwork: {
            city: 'Hamburg',
        }
    });

    function handleNameChange(e) {
        updatePerson(draft => {
            draft.name = e.target.value;
        });
    }

    function handleCityChange(e) {
        updatePerson(draft => {
            draft.artwork.city = e.target.value;
        });
    }

    return (
        <>
            <label>
                Name:
                <input value={person.name} onChange={handleNameChange} />
            </label>
            <label>
                City:
                <input
                    value={person.artwork.city}
                    onChange={handleCityChange}
                />
            </label>
            <p>
                {person.name} (located in {person.artwork.city})
            </p>
        </>
    );
}
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];
const [myList, updateMyList] = useImmer(
  initialList
);

//id=序号,nextSeen=true|false,即单值框的值,
function handleToggleMyList(id, nextSeen) {
  updateMyList(draft => {
    const artwork = draft.find(a =>
      a.id === id
    );
    artwork.seen = nextSeen;
  });
}
  //ItemList是一个另外封装的组件
  return (
    <>
      <h1>艺术愿望清单</h1>
      <h2>我想看的艺术清单:</h2>
      <ItemList
        artworks={myList}
        onToggle={handleToggleMyList} />
    </>
  );

在这里插入图片描述

对 state 进行保留和重置

相同位置的相同组件会使得 state 被保留下来。这是React的一个特性,这就造成了一个问题,比如像书本这样的应用,翻页时会显示在同样的位置,如果在前一页做了操作,如果state会保留那么这个状态会带到下一页上去,就会乱套了。

对 React 来说重要的是组件在 UI 树中的位置,而不是在 JSX 中的位置!

保留

import { useState } from 'react';

export default function App1() {
    const [isFancy, setIsFancy] = useState(false);
    return (
        <div>
            {isFancy ? (
                <Counter isFancy={true} />
            ) : (
                <Counter isFancy={false} />
            )}
            <label>
                <input
                    type="checkbox"
                    checked={isFancy}
                    onChange={e => {
                        setIsFancy(e.target.checked)
                    }}
                />
                使用好看的样式
            </label>
        </div>
    );
}

function Counter({ isFancy }) {
    const [score, setScore] = useState(0);
    const [hover, setHover] = useState(false);

    let className = {color:"red"};
    if (isFancy) {
        className = {color:"yellow"};;
    }

    return (
        <div
            style={className}
            onPointerEnter={() => setHover(true)}
            onPointerLeave={() => setHover(false)}
        >
            <h1>{score}</h1>
            <button onClick={() => setScore(score + 1)}>
                加一
            </button>
        </div>
    );
}

在这里插入图片描述
当切换颜色时,上面的数字会保留之前的样式,这是因为 相同位置的相同组件会使得 state 被保留下来 。因为React认为是同一个组件。

key使用重置

如果把上面的代码,改成如下代码,则每次切换颜色都会归0,因为用key了表示是不同的组件,不能保留的原因是重新绘制时把组件给删除了,此时状态也消失了。如果需要保留的话把它隐藏即可。或是把状态提升到父类中,在重新绘制时再以参数的方式传递进来。

{isFancy ? (
       <Counter key="blue" isFancy={true} />
   ) : (
       <Counter key="yellow" isFancy={false} />
)}

用useReducer替换useState集中状态变化函数

对于拥有许多状态更新逻辑的组件来说,过于分散的事件处理程序可能会令人不知所措。对于这种情况,你可以将组件的所有状态更新逻辑整合到一个外部函数中,这个函数叫作 reducer。

//替换对象
import { useReducer } from 'react';

/*
* action:action自定义的对象
* currState:当前状态
* */
function tasksReducer(currState, action) {
    switch (action.type) {
        case 'added': {
            console.log(action.id);
            return currState+1;
        }
        case 'deleted': {
            return currState-1;
        }
        default: {
            throw Error('未知 action:' + action.type);
        }
    }
}

export default function UseState() {
    //定义状态, tasksReducer为自定义的函数
    const [score, dispatch] = useReducer(tasksReducer, 0);

    function added(){
        // "action" 对象:它是一个普通的 JavaScript 对象。它的结构是由你决定的
        dispatch({
            type: 'added',
            id:11  //多余的参数,直接封装在action中
        })
    }
    function deleted(){
        dispatch({
            type: 'deleted',
        })
    }
    return (
        <div>
            <input type="text" value={score}/>
            <button onClick={added} >+1</button>
            <button onClick={deleted} >-1</button>
        </div>
    );
}

因为 Reducer 和 Context 就是操作state的,所以可以结合这两个元素在一起,提供一个更复杂的状态管理,比如下面这样的示例代码:

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

严格模式

严格模式主要用于开发环境帮助找到错误,在生产环境下不生效。在严格模式下开发时,它将会调用每个组件函数两次。通过重复调用组件函数,严格模式有助于找到违反一些规则的代码,比如不纯粹的组件等。

import React from 'react';
root.render(
  <React.StrictMode>  {/* 严格模式 */}
    <App />
    <Fun/>
  </React.StrictMode>
);

组件导出和导入

可以直接写成下面这样的代码,因为我们的宗旨是构建小而美的组件,所以下列的代码完全合理:

//helloReact.js
export default function Gallery(){ }
export function MyComponent(){ }
语法导出语句导入语句
默认export default function Button() {}import Button from ‘./Button.js’; 这里的Button名字可以自定义
具名export function Button() {}import { Button } from ‘./Button.js’;
  • 最后的.js可以省略
  • 同一文件中,有且仅有一个默认导出,但可以有多个具名导出!
  • 默认导出时,在导入时的名字可以自定义,但具名导入时必须和导出的名字一样,但具名导出一般多用于工具类的时候会比较多,比如下面这个工具类:
export function getImageUrl(person, size = 's') {
  return (
    'https://i.imgur.com/' +
    person.imageId +
    size +
    '.jpg'
  );
}

特殊用法

以下这些用法有时会用不到,但还是需要了解下

纯数据组件

//quotes.js
export default [
  "Don’t let yesterday take up too much of today.” — Will Rogers",
  "Ambition is putting a ladder against the sky.",
  "A joy that's shared is a joy made double.",
  ];

//InspirationGenerator.js
import quotes from './quotes';
export default function InspirationGenerator({children}) {
    const quote = quotes[index];

    return (
        <>
            <FancyText text={quote} />
        </>
    );
}  
  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

korgs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值